Merge branch 'master' into multi-queueing-modes

This commit is contained in:
smoogipoo 2021-11-10 15:01:13 +09:00
commit 35a5182ebf
23 changed files with 748 additions and 98 deletions

View File

@ -5,7 +5,7 @@ updates:
schedule:
interval: monthly
time: "17:00"
open-pull-requests-limit: 99
open-pull-requests-limit: 0 # disabled until https://github.com/dependabot/dependabot-core/issues/369 is resolved.
ignore:
- dependency-name: Microsoft.EntityFrameworkCore.Design
versions:

View File

@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public class TestSceneEditorSaving : OsuGameTestScene
{
private Screens.Edit.Editor editor => Game.ChildrenOfType<Screens.Edit.Editor>().FirstOrDefault();
private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap));
/// <summary>
/// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select.
/// Emphasis is placed on <see cref="BeatmapDifficulty.SliderMultiplier"/>, since taiko has special handling for it to keep compatibility with stable.
/// </summary>
[Test]
public void TestNewBeatmapSaveThenLoad()
{
AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType<MetadataSection>().FirstOrDefault()?.IsLoaded == true);
// We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true);
AddStep("Set slider multiplier", () => editorBeatmap.Difficulty.SliderMultiplier = 2);
AddStep("Set artist and title", () =>
{
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
});
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty");
checkMutations();
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
checkMutations();
AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
PushAndConfirm(() => new PlaySongSelect());
AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault);
AddStep("Open options", () => InputManager.Key(Key.F3));
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
checkMutations();
}
private void checkMutations()
{
AddAssert("Beatmap has correct slider multiplier", () =>
{
// we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use.
// therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct.
var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty();
taikoDifficulty.CopyFrom(editorBeatmap.Difficulty);
return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2);
});
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.Version == "difficulty");
}
}
}

View File

@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
internal class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
{
public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
{
@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
base.CopyTo(other);
if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
other.SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
public override void CopyFrom(IBeatmapDifficultyInfo other)

View File

@ -9,8 +9,11 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osuTK;
@ -20,6 +23,8 @@ namespace osu.Game.Tests.Visual.Beatmaps
{
public class TestSceneBeatmapCard : OsuTestScene
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private APIBeatmapSet[] testCases;
#region Test case generation
@ -164,6 +169,19 @@ namespace osu.Game.Tests.Visual.Beatmaps
#endregion
[SetUpSteps]
public void SetUpSteps()
{
AddStep("register request handling", () => dummyAPI.HandleRequest = request =>
{
if (!(request is PostBeatmapFavouriteRequest))
return false;
request.TriggerSuccess();
return true;
});
}
private Drawable createContent(OverlayColourScheme colourScheme, Func<APIBeatmapSet, Drawable> creationFunc)
{
var colourProvider = new OverlayColourProvider(colourScheme);

View File

@ -0,0 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Beatmaps
{
public class TestSceneBeatmapCardFavouriteButton : OsuManualInputManagerTestScene
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Test]
public void TestInitialState([Values] bool favourited)
{
APIBeatmapSet beatmapSetInfo = null;
FavouriteButton button = null;
AddStep("create beatmap set", () =>
{
beatmapSetInfo = CreateAPIBeatmapSet(Ruleset.Value);
beatmapSetInfo.HasFavourited = favourited;
});
AddStep("create button", () => Child = button = new FavouriteButton(beatmapSetInfo) { Scale = new Vector2(2) });
assertCorrectIcon(favourited);
AddAssert("correct tooltip text", () => button.TooltipText == (favourited ? BeatmapsetsStrings.ShowDetailsUnfavourite : BeatmapsetsStrings.ShowDetailsFavourite));
}
[Test]
public void TestRequestHandling()
{
APIBeatmapSet beatmapSetInfo = null;
FavouriteButton button = null;
BeatmapFavouriteAction? lastRequestAction = null;
AddStep("create beatmap set", () => beatmapSetInfo = CreateAPIBeatmapSet(Ruleset.Value));
AddStep("create button", () => Child = button = new FavouriteButton(beatmapSetInfo) { Scale = new Vector2(2) });
assertCorrectIcon(false);
AddStep("register request handling", () => dummyAPI.HandleRequest = request =>
{
if (!(request is PostBeatmapFavouriteRequest favouriteRequest))
return false;
lastRequestAction = favouriteRequest.Action;
request.TriggerSuccess();
return true;
});
AddStep("click icon", () =>
{
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("favourite request sent", () => lastRequestAction == BeatmapFavouriteAction.Favourite);
assertCorrectIcon(true);
AddStep("click icon", () =>
{
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("unfavourite request sent", () => lastRequestAction == BeatmapFavouriteAction.UnFavourite);
assertCorrectIcon(false);
}
private void assertCorrectIcon(bool favourited) => AddAssert("icon correct",
() => this.ChildrenOfType<SpriteIcon>().Single().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart));
}
}

View File

@ -13,10 +13,17 @@ namespace osu.Game.Tests.Visual.Components
{
public class TestScenePreviewTrackManager : OsuTestScene, IPreviewTrackOwner
{
private readonly TestPreviewTrackManager trackManager = new TestPreviewTrackManager();
private readonly IAdjustableAudioComponent gameTrackAudio = new AudioAdjustments();
private readonly TestPreviewTrackManager trackManager;
private AudioManager audio;
public TestScenePreviewTrackManager()
{
trackManager = new TestPreviewTrackManager(gameTrackAudio);
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@ -151,19 +158,19 @@ namespace osu.Game.Tests.Visual.Components
audio.VolumeTrack.Value = 1;
});
AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0);
AddAssert("game not muted", () => gameTrackAudio.AggregateVolume.Value != 0);
AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start track", () => track.Start());
AddAssert("game is muted", () => audio.Tracks.AggregateVolume.Value == 0);
AddAssert("game is muted", () => gameTrackAudio.AggregateVolume.Value == 0);
if (stopAnyPlaying)
AddStep("stop any playing", () => trackManager.StopAnyPlaying(owner));
else
AddStep("stop track", () => track.Stop());
AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0);
AddAssert("game not muted", () => gameTrackAudio.AggregateVolume.Value != 0);
}
[Test]
@ -224,6 +231,11 @@ namespace osu.Game.Tests.Visual.Components
public new PreviewTrack CurrentTrack => base.CurrentTrack;
public TestPreviewTrackManager(IAdjustableAudioComponent mainTrackAdjustments)
: base(mainTrackAdjustments)
{
}
protected override TrackManagerPreviewTrack CreatePreviewTrack(IBeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TestPreviewTrack(beatmapSetInfo, trackStore);
public override bool UpdateSubTree()

View File

@ -0,0 +1,98 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneUnstableRateCounter : OsuTestScene
{
[Cached(typeof(ScoreProcessor))]
private TestScoreProcessor scoreProcessor = new TestScoreProcessor();
private readonly OsuHitWindows hitWindows = new OsuHitWindows();
private UnstableRateCounter counter;
private double prev;
[SetUpSteps]
public void SetUp()
{
AddStep("Reset Score Processor", () => scoreProcessor.Reset());
}
[Test]
public void TestBasic()
{
AddStep("Create Display", recreateDisplay);
// Needs multiples 2 by the nature of UR, and went for 4 to be safe.
// Creates a 250 UR by placing a +25ms then a -25ms judgement, which then results in a 250 UR
AddRepeatStep("Set UR to 250", () => applyJudgement(25, true), 4);
AddUntilStep("UR = 250", () => counter.Current.Value == 250.0);
AddRepeatStep("Revert UR", () =>
{
scoreProcessor.RevertResult(
new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
{
TimeOffset = 25,
Type = HitResult.Perfect,
});
}, 4);
AddUntilStep("UR is 0", () => counter.Current.Value == 0.0);
AddUntilStep("Counter is invalid", () => counter.Child.Alpha == 0.3f);
//Sets a UR of 0 by creating 10 10ms offset judgements. Since average = offset, UR = 0
AddRepeatStep("Set UR to 0", () => applyJudgement(10, false), 10);
//Applies a UR of 100 by creating 10 -10ms offset judgements. At the 10th judgement, offset should be 100.
AddRepeatStep("Bring UR to 100", () => applyJudgement(-10, false), 10);
}
private void recreateDisplay()
{
Clear();
Add(counter = new UnstableRateCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(5),
});
}
private void applyJudgement(double offsetMs, bool alt)
{
double placement = offsetMs;
if (alt)
{
placement = prev > 0 ? -offsetMs : offsetMs;
prev = placement;
}
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
{
TimeOffset = placement,
Type = HitResult.Perfect,
});
}
private class TestScoreProcessor : ScoreProcessor
{
public void Reset() => base.Reset(false);
}
}
}

View File

@ -1,13 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Mixing;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -19,27 +14,26 @@ namespace osu.Game.Audio
{
public class PreviewTrackManager : Component
{
private readonly IAdjustableAudioComponent mainTrackAdjustments;
private readonly BindableDouble muteBindable = new BindableDouble();
[Resolved]
private AudioManager audio { get; set; }
private PreviewTrackStore trackStore;
private ITrackStore trackStore;
protected TrackManagerPreviewTrack CurrentTrack;
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(OsuGameBase.GLOBAL_TRACK_VOLUME_ADJUST);
public PreviewTrackManager(IAdjustableAudioComponent mainTrackAdjustments)
{
this.mainTrackAdjustments = mainTrackAdjustments;
}
[BackgroundDependencyLoader]
private void load(AudioManager audioManager)
{
// this is a temporary solution to get around muting ourselves.
// todo: update this once we have a BackgroundTrackManager or similar.
trackStore = new PreviewTrackStore(audioManager.TrackMixer, new OnlineStore());
audio.AddItem(trackStore);
trackStore.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust);
trackStore.AddAdjustment(AdjustableProperty.Volume, audio.VolumeTrack);
trackStore = audioManager.GetTrackStore(new OnlineStore());
}
/// <summary>
@ -55,7 +49,7 @@ namespace osu.Game.Audio
{
CurrentTrack?.Stop();
CurrentTrack = track;
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
mainTrackAdjustments.AddAdjustment(AdjustableProperty.Volume, muteBindable);
});
track.Stopped += () => Schedule(() =>
@ -64,7 +58,7 @@ namespace osu.Game.Audio
return;
CurrentTrack = null;
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
mainTrackAdjustments.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
});
return track;
@ -116,52 +110,5 @@ namespace osu.Game.Audio
protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo.OnlineID}.mp3");
}
private class PreviewTrackStore : AudioCollectionManager<AdjustableAudioComponent>, ITrackStore
{
private readonly AudioMixer defaultMixer;
private readonly IResourceStore<byte[]> store;
internal PreviewTrackStore(AudioMixer defaultMixer, IResourceStore<byte[]> store)
{
this.defaultMixer = defaultMixer;
this.store = store;
}
public Track GetVirtual(double length = double.PositiveInfinity)
{
if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}");
var track = new TrackVirtual(length);
AddItem(track);
return track;
}
public Track Get(string name)
{
if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}");
if (string.IsNullOrEmpty(name)) return null;
var dataStream = store.GetStream(name);
if (dataStream == null)
return null;
// Todo: This is quite unsafe. TrackBass shouldn't be exposed as public.
Track track = new TrackBass(dataStream);
defaultMixer.Add(track);
AddItem(track);
return track;
}
public Task<Track> GetAsync(string name) => Task.Run(() => Get(name));
public Stream GetStream(string name) => store.GetStream(name);
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
}
}
}

View File

@ -10,6 +10,8 @@ using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Mixing;
using osu.Framework.Audio.Track;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Testing;
@ -30,18 +32,23 @@ namespace osu.Game.Beatmaps
[ExcludeFromDynamicCompile]
public class BeatmapManager : IModelDownloader<IBeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache, IDisposable
{
public ITrackStore BeatmapTrackStore { get; }
private readonly BeatmapModelManager beatmapModelManager;
private readonly BeatmapModelDownloader beatmapModelDownloader;
private readonly WorkingBeatmapCache workingBeatmapCache;
private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null,
WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false, AudioMixer mainTrackMixer = null)
{
var userResources = new FileStore(contextFactory, storage).Store;
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host);
beatmapModelDownloader = CreateBeatmapModelDownloader(beatmapModelManager, api, host);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
workingBeatmapCache.BeatmapManager = beatmapModelManager;
beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache;
@ -58,8 +65,10 @@ namespace osu.Game.Beatmaps
return new BeatmapModelDownloader(modelManager, api, host);
}
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host) =>
new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host);
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host)
{
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
}
protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) =>
new BeatmapModelManager(storage, contextFactory, rulesets, host);

View File

@ -189,7 +189,11 @@ namespace osu.Game.Beatmaps
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
// This should hopefully be temporary, assuming said clone is eventually removed.
beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty);
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
// CopyTo() will undo such adjustments, while CopyFrom() will not.
beatmapContent.Difficulty.CopyTo(beatmapInfo.BaseDifficulty);
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;

View File

@ -3,12 +3,14 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@ -21,6 +23,7 @@ using osuTK;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
using DownloadButton = osu.Game.Beatmaps.Drawables.Cards.Buttons.DownloadButton;
namespace osu.Game.Beatmaps.Drawables.Cards
{
@ -33,9 +36,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private const float corner_radius = 10;
private readonly APIBeatmapSet beatmapSet;
private readonly Bindable<BeatmapSetFavouriteState> favouriteState;
private UpdateableOnlineBeatmapSetCover leftCover;
private FillFlowContainer iconArea;
private FillFlowContainer leftIconArea;
private Container rightButtonArea;
private Container mainContent;
private BeatmapCardContentBackground mainContentBackground;
@ -51,6 +57,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
: base(HoverSampleSet.Submit)
{
this.beatmapSet = beatmapSet;
favouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
}
[BackgroundDependencyLoader]
@ -79,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.Both,
OnlineInfo = beatmapSet
},
iconArea = new FillFlowContainer
leftIconArea = new FillFlowContainer
{
Margin = new MarginPadding(5),
AutoSizeAxes = Axes.Both,
@ -88,6 +95,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}
}
},
rightButtonArea = new Container
{
Name = @"Right (button) area",
Width = 30,
RelativeSizeAxes = Axes.Y,
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Child = new FillFlowContainer<BeatmapCardIconButton>
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 14),
Children = new BeatmapCardIconButton[]
{
new FavouriteButton(beatmapSet) { Current = favouriteState },
new DownloadButton(beatmapSet)
}
}
},
mainContent = new Container
{
Name = @"Main content",
@ -226,10 +254,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
};
if (beatmapSet.HasVideo)
iconArea.Add(new IconPill(FontAwesome.Solid.Film));
leftIconArea.Add(new IconPill(FontAwesome.Solid.Film));
if (beatmapSet.HasStoryboard)
iconArea.Add(new IconPill(FontAwesome.Solid.Image));
leftIconArea.Add(new IconPill(FontAwesome.Solid.Image));
if (beatmapSet.HasExplicitContent)
{
@ -287,7 +315,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
if (beatmapSet.HypeStatus != null && beatmapSet.NominationStatus != null)
yield return new NominationsStatistic(beatmapSet.NominationStatus);
yield return new FavouritesStatistic(beatmapSet);
yield return new FavouritesStatistic(beatmapSet) { Current = favouriteState };
yield return new PlayCountStatistic(beatmapSet);
var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet);
@ -306,6 +334,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
leftCover.FadeColour(IsHovered ? OsuColour.Gray(0.2f) : Color4.White, TRANSITION_DURATION, Easing.OutQuint);
statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
rightButtonArea.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
namespace osu.Game.Beatmaps.Drawables.Cards
{
/// <summary>
/// Stores the current favourite state of a beatmap set.
/// Used to coordinate between <see cref="FavouriteButton"/> and <see cref="FavouritesStatistic"/>.
/// </summary>
public readonly struct BeatmapSetFavouriteState
{
/// <summary>
/// Whether the currently logged-in user has favourited this beatmap.
/// </summary>
public bool Favourited { get; }
/// <summary>
/// The number of favourites that the beatmap set has received, including the currently logged-in user.
/// </summary>
public int FavouriteCount { get; }
public BeatmapSetFavouriteState(bool favourited, int favouriteCount)
{
Favourited = favourited;
FavouriteCount = favouriteCount;
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
public abstract class BeatmapCardIconButton : OsuHoverContainer
{
protected readonly SpriteIcon Icon;
private float size;
public new float Size
{
get => size;
set
{
size = value;
Icon.Size = new Vector2(size);
}
}
protected BeatmapCardIconButton()
{
Add(Icon = new SpriteIcon());
AutoSizeAxes = Axes.Both;
Size = 12;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Anchor = Origin = Anchor.Centre;
IdleColour = colourProvider.Light1;
HoverColour = colourProvider.Content1;
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
public class DownloadButton : BeatmapCardIconButton
{
public DownloadButton(APIBeatmapSet beatmapSet)
{
Icon.Icon = FontAwesome.Solid.FileDownload;
}
// TODO: implement behaviour
}
}

View File

@ -0,0 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
public class FavouriteButton : BeatmapCardIconButton, IHasCurrentValue<BeatmapSetFavouriteState>
{
private readonly BindableWithCurrent<BeatmapSetFavouriteState> current;
public Bindable<BeatmapSetFavouriteState> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly APIBeatmapSet beatmapSet;
private PostBeatmapFavouriteRequest favouriteRequest;
[Resolved]
private IAPIProvider api { get; set; }
public FavouriteButton(APIBeatmapSet beatmapSet)
{
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
this.beatmapSet = beatmapSet;
}
protected override void LoadComplete()
{
base.LoadComplete();
Action = toggleFavouriteStatus;
current.BindValueChanged(_ => updateState(), true);
}
private void toggleFavouriteStatus()
{
var actionType = current.Value.Favourited ? BeatmapFavouriteAction.UnFavourite : BeatmapFavouriteAction.Favourite;
favouriteRequest?.Cancel();
favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, actionType);
Enabled.Value = false;
favouriteRequest.Success += () =>
{
bool favourited = actionType == BeatmapFavouriteAction.Favourite;
current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1));
Enabled.Value = true;
};
favouriteRequest.Failure += e =>
{
Logger.Error(e, $"Failed to {actionType.ToString().ToLower()} beatmap: {e.Message}");
Enabled.Value = true;
};
api.Queue(favouriteRequest);
}
private void updateState()
{
if (current.Value.Favourited)
{
Icon.Icon = FontAwesome.Solid.Heart;
TooltipText = BeatmapsetsStrings.ShowDetailsUnfavourite;
}
else
{
Icon.Icon = FontAwesome.Regular.Heart;
TooltipText = BeatmapsetsStrings.ShowDetailsFavourite;
}
}
}
}

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
@ -11,13 +13,32 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
/// <summary>
/// Shows the number of favourites that a beatmap set has received.
/// </summary>
public class FavouritesStatistic : BeatmapCardStatistic
public class FavouritesStatistic : BeatmapCardStatistic, IHasCurrentValue<BeatmapSetFavouriteState>
{
private readonly BindableWithCurrent<BeatmapSetFavouriteState> current;
public Bindable<BeatmapSetFavouriteState> Current
{
get => current.Current;
set => current.Current = value;
}
public FavouritesStatistic(IBeatmapSetOnlineInfo onlineInfo)
{
Icon = onlineInfo.HasFavourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart;
Text = onlineInfo.FavouriteCount.ToMetric(decimals: 1);
TooltipText = BeatmapsStrings.PanelFavourites(onlineInfo.FavouriteCount.ToLocalisableString(@"N0"));
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(onlineInfo.HasFavourited, onlineInfo.FavouriteCount));
}
protected override void LoadComplete()
{
base.LoadComplete();
current.BindValueChanged(_ => updateState(), true);
}
private void updateState()
{
Icon = current.Value.Favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart;
Text = current.Value.FavouriteCount.ToMetric(decimals: 1);
TooltipText = BeatmapsStrings.PanelFavourites(current.Value.FavouriteCount.ToLocalisableString(@"N0"));
}
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Beatmaps
[CanBeNull]
private readonly GameHost host;
public WorkingBeatmapCache([NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null, GameHost host = null)
public WorkingBeatmapCache(ITrackStore trackStore, AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null, GameHost host = null)
{
DefaultBeatmap = defaultBeatmap;
@ -50,7 +50,7 @@ namespace osu.Game.Beatmaps
this.host = host;
this.files = files;
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files));
trackStore = audioManager.GetTrackStore(files);
this.trackStore = trackStore;
}
public void Invalidate(BeatmapSetInfo info)

View File

@ -209,7 +209,13 @@ namespace osu.Game.Database
case 9:
// Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well.
var oldMetadata = migration.OldRealm.DynamicApi.All(getMappedOrOriginalName(typeof(RealmBeatmapMetadata)));
string metadataClassName = getMappedOrOriginalName(typeof(RealmBeatmapMetadata));
// May be coming from a version before `RealmBeatmapMetadata` existed.
if (!migration.OldRealm.Schema.TryFindObjectSchema(metadataClassName, out _))
return;
var oldMetadata = migration.OldRealm.DynamicApi.All(metadataClassName);
var newMetadata = migration.NewRealm.All<RealmBeatmapMetadata>();
int metadataCount = newMetadata.Count();

View File

@ -8,20 +8,21 @@ namespace osu.Game.Online.API.Requests
{
public class PostBeatmapFavouriteRequest : APIRequest
{
public readonly BeatmapFavouriteAction Action;
private readonly int id;
private readonly BeatmapFavouriteAction action;
public PostBeatmapFavouriteRequest(int id, BeatmapFavouriteAction action)
{
this.id = id;
this.action = action;
Action = action;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.AddParameter(@"action", action.ToString().ToLowerInvariant());
req.AddParameter(@"action", Action.ToString().ToLowerInvariant());
return req;
}

View File

@ -65,7 +65,7 @@ namespace osu.Game
/// <summary>
/// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects.
/// </summary>
internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.8;
private const double global_track_volume_adjust = 0.8;
public bool UseDevelopmentServer { get; }
@ -159,7 +159,7 @@ namespace osu.Game
private Bindable<bool> fpsDisplayVisible;
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(GLOBAL_TRACK_VOLUME_ADJUST);
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(global_track_volume_adjust);
private RealmRulesetStore realmRulesetStore;
@ -315,7 +315,7 @@ namespace osu.Game
dependencies.Cache(globalBindings);
PreviewTrackManager previewTrackManager;
dependencies.Cache(previewTrackManager = new PreviewTrackManager());
dependencies.Cache(previewTrackManager = new PreviewTrackManager(BeatmapManager.BeatmapTrackStore));
Add(previewTrackManager);
AddInternal(MusicController = new MusicController());

View File

@ -153,12 +153,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
new CancelButton { Action = finalise },
new ClearButton { Action = clear },
},
}
},
new HoverClickSounds()
}
}
}
},
new HoverClickSounds()
}
};
foreach (var b in bindings)

View File

@ -0,0 +1,147 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
public class UnstableRateCounter : RollingCounter<int>, ISkinnableDrawable
{
public bool UsesFixedAnchor { get; set; }
protected override double RollingDuration => 750;
private const float alpha_when_invalid = 0.3f;
private readonly Bindable<bool> valid = new Bindable<bool>();
private readonly List<double> hitOffsets = new List<double>();
[Resolved]
private ScoreProcessor scoreProcessor { get; set; }
public UnstableRateCounter()
{
Current.Value = 0;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache)
{
Colour = colours.BlueLighter;
valid.BindValueChanged(e =>
DrawableCount.FadeTo(e.NewValue ? 1 : alpha_when_invalid, 1000, Easing.OutQuint));
}
private bool changesUnstableRate(JudgementResult judgement)
=> !(judgement.HitObject.HitWindows is HitWindows.EmptyHitWindows) && judgement.IsHit;
protected override void LoadComplete()
{
base.LoadComplete();
scoreProcessor.NewJudgement += onJudgementAdded;
scoreProcessor.JudgementReverted += onJudgementReverted;
}
private void onJudgementAdded(JudgementResult judgement)
{
if (!changesUnstableRate(judgement)) return;
hitOffsets.Add(judgement.TimeOffset);
updateDisplay();
}
private void onJudgementReverted(JudgementResult judgement)
{
if (judgement.FailedAtJudgement || !changesUnstableRate(judgement)) return;
hitOffsets.RemoveAt(hitOffsets.Count - 1);
updateDisplay();
}
private void updateDisplay()
{
// At Count = 0, we get NaN, While we are allowing count = 1, it will be 0 since average = offset.
if (hitOffsets.Count > 0)
{
double mean = hitOffsets.Average();
double squares = hitOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum();
Current.Value = (int)(Math.Sqrt(squares / hitOffsets.Count) * 10);
valid.Value = true;
}
else
{
Current.Value = 0;
valid.Value = false;
}
}
protected override IHasText CreateText() => new TextComponent
{
Alpha = alpha_when_invalid,
};
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (scoreProcessor == null) return;
scoreProcessor.NewJudgement -= onJudgementAdded;
scoreProcessor.JudgementReverted -= onJudgementReverted;
}
private class TextComponent : CompositeDrawable, IHasText
{
public LocalisableString Text
{
get => text.Text;
set => text.Text = value;
}
private readonly OsuSpriteText text;
public TextComponent()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
text = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Numeric.With(size: 16, fixedWidth: true)
},
new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Numeric.With(size: 8, fixedWidth: true),
Text = "UR",
Padding = new MarginPadding { Bottom = 1.5f },
}
}
};
}
}
}
}

View File

@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual
private readonly TestBeatmapManager testBeatmapManager;
public TestWorkingBeatmapCache(TestBeatmapManager testBeatmapManager, AudioManager audioManager, IResourceStore<byte[]> resourceStore, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost gameHost)
: base(audioManager, resourceStore, storage, defaultBeatmap, gameHost)
: base(testBeatmapManager.BeatmapTrackStore, audioManager, resourceStore, storage, defaultBeatmap, gameHost)
{
this.testBeatmapManager = testBeatmapManager;
}