From 411252e004f15f51488b67cab5909372f99490d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 17:01:27 +0900 Subject: [PATCH 01/30] Replace squirrel fork with `Clowd.Squirrel` --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index b1117bf796..32ead231c7 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,10 +24,10 @@ + - all From 6a4d731eb3a94c991cdea9a4f461ead85f31d5d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 17:11:24 +0900 Subject: [PATCH 02/30] Update obsolete usages in line with `Clowd.Squirrel` changes --- osu.Desktop/OsuGameDesktop.cs | 3 +++ osu.Desktop/Updater/SquirrelUpdateManager.cs | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index cd3fb7eb61..be8159a7cc 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -96,6 +97,8 @@ namespace osu.Desktop switch (RuntimeInfo.OS) { case RuntimeInfo.Platform.Windows: + Debug.Assert(OperatingSystem.IsWindows()); + return new SquirrelUpdateManager(); default: diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 7b60bc03e4..b307146b10 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Runtime.Versioning; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,10 +17,11 @@ using osu.Game.Overlays.Notifications; using osuTK; using osuTK.Graphics; using Squirrel; -using LogLevel = Splat.LogLevel; +using Squirrel.SimpleSplat; namespace osu.Desktop.Updater { + [SupportedOSPlatform("windows")] public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager { private UpdateManager updateManager; @@ -34,12 +36,14 @@ namespace osu.Desktop.Updater /// private bool updatePending; + private readonly SquirrelLogger squirrelLogger = new SquirrelLogger(); + [BackgroundDependencyLoader] private void load(NotificationOverlay notification) { notificationOverlay = notification; - Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); + SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger)); } protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); @@ -49,9 +53,11 @@ namespace osu.Desktop.Updater // should we schedule a retry on completion of this check? bool scheduleRecheck = true; + const string github_token = null; // TODO: populate. + try { - updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false); + updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer"); var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); @@ -201,11 +207,11 @@ namespace osu.Desktop.Updater } } - private class SquirrelLogger : Splat.ILogger, IDisposable + private class SquirrelLogger : ILogger, IDisposable { - public LogLevel Level { get; set; } = LogLevel.Info; + public Squirrel.SimpleSplat.LogLevel Level { get; set; } = Squirrel.SimpleSplat.LogLevel.Info; - public void Write(string message, LogLevel logLevel) + public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel) { if (logLevel < Level) return; From 1c705f3b331e4fab9e33f4465ab3a4611c4f5916 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 17:31:45 +0900 Subject: [PATCH 03/30] Mark `osu.Desktop` as squirrel-aware --- osu.Desktop/app.manifest | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Desktop/app.manifest b/osu.Desktop/app.manifest index 2e9127bf44..a11cee132c 100644 --- a/osu.Desktop/app.manifest +++ b/osu.Desktop/app.manifest @@ -1,6 +1,7 @@ + 1 @@ -17,4 +18,4 @@ true - \ No newline at end of file + From 3aa2d4548ab40cca9b023a913c49719a43a57c6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Mar 2022 17:54:33 +0900 Subject: [PATCH 04/30] Add startup squirrel icon/association handling --- osu.Desktop/Program.cs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index b944068e78..0e11e172e1 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using osu.Desktop.LegacyIpc; @@ -12,6 +13,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IPC; using osu.Game.Tournament; +using Squirrel; namespace osu.Desktop { @@ -24,6 +26,10 @@ namespace osu.Desktop [STAThread] public static void Main(string[] args) { + // run Squirrel first, as the app may exit after these run + if (OperatingSystem.IsWindows()) + setupSquirrel(); + // Back up the cwd before DesktopGameHost changes it string cwd = Environment.CurrentDirectory; @@ -104,6 +110,25 @@ namespace osu.Desktop } } + [SupportedOSPlatform("windows")] + private static void setupSquirrel() + { + SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) => + { + tools.CreateShortcutForThisExe(); + }, onAppUninstall: (version, tools) => + { + tools.RemoveShortcutForThisExe(); + tools.RemoveUninstallerRegistryEntry(); + }, onEveryRun: (version, tools, firstRun) => + { + tools.SetProcessAppUserModelId(); + + if (firstRun) + tools.CreateUninstallerRegistryEntry(); + }); + } + private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1; /// From e14a35b469de3841c3d573758b9a5266d99abb93 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Mar 2022 20:32:03 +0300 Subject: [PATCH 05/30] Add failing test case --- .../Gameplay/TestSceneStoryboardSamples.cs | 59 ------------- .../TestSceneStoryboardSamplePlayback.cs | 83 +++++++++++++++++-- osu.Game/Tests/Visual/TestPlayer.cs | 3 - 3 files changed, 78 insertions(+), 67 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 88862ea28b..6457a23a1b 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -1,29 +1,23 @@ // Copyright (c) ppy Pty Ltd . 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.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Testing; -using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Skinning; @@ -118,59 +112,6 @@ namespace osu.Game.Tests.Gameplay AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue); } - [TestCase(typeof(OsuModDoubleTime), 1.5)] - [TestCase(typeof(OsuModHalfTime), 0.75)] - [TestCase(typeof(ModWindUp), 1.5)] - [TestCase(typeof(ModWindDown), 0.75)] - [TestCase(typeof(OsuModDoubleTime), 2)] - [TestCase(typeof(OsuModHalfTime), 0.5)] - [TestCase(typeof(ModWindUp), 2)] - [TestCase(typeof(ModWindDown), 0.5)] - public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate) - { - GameplayClockContainer gameplayContainer = null; - StoryboardSampleInfo sampleInfo = null; - TestDrawableStoryboardSample sample = null; - - Mod testedMod = Activator.CreateInstance(expectedMod) as Mod; - - switch (testedMod) - { - case ModRateAdjust m: - m.SpeedChange.Value = expectedRate; - break; - - case ModTimeRamp m: - m.FinalRate.Value = m.InitialRate.Value = expectedRate; - break; - } - - AddStep("setup storyboard sample", () => - { - Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, this); - SelectedMods.Value = new[] { testedMod }; - - var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); - - Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) - { - Child = beatmapSkinSourceContainer - }); - - beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(sampleInfo = new StoryboardSampleInfo("test-sample", 1, 1)) - { - Clock = gameplayContainer.GameplayClock - }); - }); - - AddStep("start", () => gameplayContainer.Start()); - - AddAssert("sample playback rate matches mod rates", () => - testedMod != null && Precision.AlmostEquals( - sample.ChildrenOfType().First().AggregateFrequency.Value, - ((IApplicableToRate)testedMod).ApplyToRate(sampleInfo.StartTime))); - } - [Test] public void TestSamplePlaybackWithBeatmapHitsoundsOff() { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index 95603b5c04..7a74a00c68 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -1,17 +1,23 @@ // Copyright (c) ppy Pty Ltd . 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 NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Audio; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { @@ -19,6 +25,10 @@ namespace osu.Game.Tests.Visual.Gameplay { private Storyboard storyboard; + private IReadOnlyList storyboardMods; + + protected override bool HasCustomSteps => true; + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -31,10 +41,13 @@ namespace osu.Game.Tests.Visual.Gameplay backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20)); } + [SetUp] + public void SetUp() => Schedule(() => storyboardMods = Array.Empty()); + [Test] public void TestStoryboardSamplesStopDuringPause() { - checkForFirstSamplePlayback(); + createPlayerTest(); AddStep("player paused", () => Player.Pause()); AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value); @@ -47,26 +60,86 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestStoryboardSamplesStopOnSkip() { - checkForFirstSamplePlayback(); + createPlayerTest(true); - AddStep("skip intro", () => InputManager.Key(osuTK.Input.Key.Space)); AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); AddUntilStep("any storyboard samples playing after skip", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); } - private void checkForFirstSamplePlayback() + [TestCase(typeof(OsuModDoubleTime), 1.5)] + [TestCase(typeof(OsuModDoubleTime), 2)] + [TestCase(typeof(OsuModHalfTime), 0.75)] + [TestCase(typeof(OsuModHalfTime), 0.5)] + public void TestStoryboardSamplesPlaybackWithRateAdjustMods(Type expectedMod, double expectedRate) { + AddStep("setup mod", () => + { + ModRateAdjust testedMod = (ModRateAdjust)Activator.CreateInstance(expectedMod).AsNonNull(); + testedMod.SpeedChange.Value = expectedRate; + storyboardMods = new[] { testedMod }; + }); + + createPlayerTest(true); + + AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound => + { + return sound.ChildrenOfType().All(s => s.AggregateFrequency.Value == expectedRate); + })); + } + + [TestCase(typeof(ModWindUp), 0.5, 2)] + [TestCase(typeof(ModWindUp), 1.51, 2)] + [TestCase(typeof(ModWindDown), 2, 0.5)] + [TestCase(typeof(ModWindDown), 0.99, 0.5)] + public void TestStoryboardSamplesPlaybackWithTimeRampMods(Type expectedMod, double initialRate, double finalRate) + { + AddStep("setup mod", () => + { + ModTimeRamp testedMod = (ModTimeRamp)Activator.CreateInstance(expectedMod).AsNonNull(); + testedMod.InitialRate.Value = initialRate; + testedMod.FinalRate.Value = finalRate; + storyboardMods = new[] { testedMod }; + }); + + createPlayerTest(true); + + ModTimeRamp gameplayMod = null; + + AddUntilStep("mod speed change updated", () => + { + gameplayMod = Player.GameplayState.Mods.OfType().Single(); + return gameplayMod.SpeedChange.Value != initialRate; + }); + + AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound => + { + return sound.ChildrenOfType().All(s => s.AggregateFrequency.Value == gameplayMod.SpeedChange.Value); + })); + } + + private void createPlayerTest(bool skipIntro = false) + { + CreateTest(null); + AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null); AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + + if (skipIntro) + AddStep("skip intro", () => InputManager.Key(Key.Space)); } private IEnumerable allStoryboardSamples => Player.ChildrenOfType(); protected override bool AllowFail => false; + protected override TestPlayer CreatePlayer(Ruleset ruleset) + { + SelectedMods.Value = SelectedMods.Value.Concat(storyboardMods).ToArray(); + return new TestPlayer(true, false); + } + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false); protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new ClockBackedTestWorkingBeatmap(beatmap, storyboard ?? this.storyboard, Clock, Audio); diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 368f792e28..d463905cf4 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; - /// - /// Mods from *player* (not OsuScreen). - /// public new Bindable> Mods => base.Mods; public new HUDOverlay HUDOverlay => base.HUDOverlay; From cbb8dc28914a4c7a8cb636ea5b58534869eea934 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Mar 2022 20:33:46 +0300 Subject: [PATCH 06/30] Fix storyboard samples rate not adjusted from actual gameplay mods --- .../Visual/Gameplay/TestSceneStoryboard.cs | 4 ++-- .../Backgrounds/BeatmapBackgroundWithStoryboard.cs | 8 +++++++- osu.Game/Screens/Play/DimmableStoryboard.cs | 9 +++++++-- osu.Game/Screens/Play/Player.cs | 2 +- .../Storyboards/Drawables/DrawableStoryboard.cs | 9 ++++++++- .../Drawables/DrawableStoryboardSample.cs | 13 ++++++++----- osu.Game/Storyboards/Storyboard.cs | 5 +++-- 7 files changed, 36 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 3b6d02c67c..014ccb1652 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; storyboardContainer.Clock = decoupledClock; - storyboard = working.Storyboard.CreateDrawable(Beatmap.Value); + storyboard = working.Storyboard.CreateDrawable(SelectedMods.Value); storyboard.Passing = false; storyboardContainer.Add(storyboard); @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Gameplay sb = decoder.Decode(bfr); } - storyboard = sb.CreateDrawable(Beatmap.Value); + storyboard = sb.CreateDrawable(SelectedMods.Value); storyboardContainer.Add(storyboard); decoupledClock.ChangeSource(Beatmap.Value.Track); diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs index 56ef87c1f4..7aed442800 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackgroundWithStoryboard.cs @@ -3,12 +3,15 @@ #nullable enable +using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; using osu.Game.Storyboards.Drawables; namespace osu.Game.Graphics.Backgrounds @@ -20,6 +23,9 @@ namespace osu.Game.Graphics.Backgrounds [Resolved(CanBeNull = true)] private MusicController? musicController { get; set; } + [Resolved] + private IBindable> mods { get; set; } = null!; + public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1") : base(beatmap, fallbackTextureName) { @@ -39,7 +45,7 @@ namespace osu.Game.Graphics.Backgrounds { RelativeSizeAxes = Axes.Both, Volume = { Value = 0 }, - Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = storyboardClock } + Child = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock } }, AddInternal); } diff --git a/osu.Game/Screens/Play/DimmableStoryboard.cs b/osu.Game/Screens/Play/DimmableStoryboard.cs index f8cedddfbe..5a3ef1e9d3 100644 --- a/osu.Game/Screens/Play/DimmableStoryboard.cs +++ b/osu.Game/Screens/Play/DimmableStoryboard.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Mods; using osu.Game.Storyboards; using osu.Game.Storyboards.Drawables; @@ -18,6 +20,8 @@ namespace osu.Game.Screens.Play public Container OverlayLayerContainer { get; private set; } private readonly Storyboard storyboard; + private readonly IReadOnlyList mods; + private DrawableStoryboard drawableStoryboard; /// @@ -28,9 +32,10 @@ namespace osu.Game.Screens.Play /// public IBindable HasStoryboardEnded = new BindableBool(true); - public DimmableStoryboard(Storyboard storyboard) + public DimmableStoryboard(Storyboard storyboard, IReadOnlyList mods) { this.storyboard = storyboard; + this.mods = mods; } [BackgroundDependencyLoader] @@ -57,7 +62,7 @@ namespace osu.Game.Screens.Play if (!ShowStoryboard.Value && !IgnoreUserSettings.Value) return; - drawableStoryboard = storyboard.CreateDrawable(); + drawableStoryboard = storyboard.CreateDrawable(mods); HasStoryboardEnded.BindTo(drawableStoryboard.HasStoryboardEnded); if (async) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d4b02622d3..8ce3f1587d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -355,7 +355,7 @@ namespace osu.Game.Screens.Play protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); private Drawable createUnderlayComponents() => - DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; + DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both }; private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay) { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index e6528a83bd..840500347f 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . 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 System.Threading; using osuTK; @@ -11,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; using osu.Game.Database; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; using osu.Game.Stores; @@ -50,14 +53,18 @@ namespace osu.Game.Storyboards.Drawables private double? lastEventEndTime; + [Cached(typeof(IReadOnlyList))] + public IReadOnlyList Mods { get; } + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - public DrawableStoryboard(Storyboard storyboard) + public DrawableStoryboard(Storyboard storyboard, IReadOnlyList mods) { Storyboard = storyboard; + Mods = mods ?? Array.Empty(); Size = new Vector2(640, 480); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 672274a2ad..4e3f72512c 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -28,17 +28,20 @@ namespace osu.Game.Storyboards.Drawables LifetimeStart = sampleInfo.StartTime; } - [Resolved] - private IBindable> mods { get; set; } + [Resolved(CanBeNull = true)] + private IReadOnlyList mods { get; set; } protected override void SkinChanged(ISkinSource skin) { base.SkinChanged(skin); - foreach (var mod in mods.Value.OfType()) + if (mods != null) { - foreach (var sample in DrawableSamples) - mod.ApplyToSample(sample); + foreach (var mod in mods.OfType()) + { + foreach (var sample in DrawableSamples) + mod.ApplyToSample(sample); + } } } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index c4864c0334..2faed98ae0 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Extensions; +using osu.Game.Rulesets.Mods; using osu.Game.Skinning; using osu.Game.Storyboards.Drawables; @@ -90,8 +91,8 @@ namespace osu.Game.Storyboards } } - public DrawableStoryboard CreateDrawable(IWorkingBeatmap working = null) => - new DrawableStoryboard(this); + public DrawableStoryboard CreateDrawable(IReadOnlyList mods = null) => + new DrawableStoryboard(this, mods); public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) { From b286122413b21be910bf29c8349f3030d3e4b2f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 03:54:39 +0900 Subject: [PATCH 07/30] Move uninstaller registry operation to `onInitialInstall` --- osu.Desktop/Program.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 0e11e172e1..e317a44bc3 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -116,6 +116,7 @@ namespace osu.Desktop SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) => { tools.CreateShortcutForThisExe(); + tools.CreateUninstallerRegistryEntry(); }, onAppUninstall: (version, tools) => { tools.RemoveShortcutForThisExe(); @@ -123,9 +124,6 @@ namespace osu.Desktop }, onEveryRun: (version, tools, firstRun) => { tools.SetProcessAppUserModelId(); - - if (firstRun) - tools.CreateUninstallerRegistryEntry(); }); } From a812ed4462a5126ff9ba2eb2d858814e59a2ddee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Mar 2022 23:40:14 +0300 Subject: [PATCH 08/30] Ensure there is at least one sample during rate assertion --- .../Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index 7a74a00c68..44529aa78c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -83,9 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay createPlayerTest(true); AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound => - { - return sound.ChildrenOfType().All(s => s.AggregateFrequency.Value == expectedRate); - })); + sound.ChildrenOfType().First().AggregateFrequency.Value == expectedRate)); } [TestCase(typeof(ModWindUp), 0.5, 2)] @@ -113,9 +111,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound => - { - return sound.ChildrenOfType().All(s => s.AggregateFrequency.Value == gameplayMod.SpeedChange.Value); - })); + sound.ChildrenOfType().First().AggregateFrequency.Value == gameplayMod.SpeedChange.Value)); } private void createPlayerTest(bool skipIntro = false) From 82bbc32d74147ce99680e4a5c99e4ffed1bb00b9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Mar 2022 23:44:58 +0300 Subject: [PATCH 09/30] Remove unnecessary `Schedule` during setup --- .../Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index 44529aa78c..1d50901fec 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [SetUp] - public void SetUp() => Schedule(() => storyboardMods = Array.Empty()); + public void SetUp() => storyboardMods = Array.Empty(); [Test] public void TestStoryboardSamplesStopDuringPause() From bb94d68139478a11054f76dc0258e590997e6b4c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 2 Mar 2022 23:55:42 +0300 Subject: [PATCH 10/30] Separate storyboard samples and skip intro steps to own methods --- .../TestSceneStoryboardSamplePlayback.cs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index 1d50901fec..6b74868944 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -51,20 +51,21 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("player paused", () => Player.Pause()); AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value); - AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); + allStoryobardSamplesStopped(); AddStep("player resume", () => Player.Resume()); - AddUntilStep("any storyboard samples playing after resume", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + waitUntilStoryboardSamplesPlay(); } [Test] public void TestStoryboardSamplesStopOnSkip() { - createPlayerTest(true); + createPlayerTest(); - AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); + skipIntro(); + allStoryobardSamplesStopped(); - AddUntilStep("any storyboard samples playing after skip", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + waitUntilStoryboardSamplesPlay(); } [TestCase(typeof(OsuModDoubleTime), 1.5)] @@ -80,7 +81,8 @@ namespace osu.Game.Tests.Visual.Gameplay storyboardMods = new[] { testedMod }; }); - createPlayerTest(true); + createPlayerTest(); + skipIntro(); AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound => sound.ChildrenOfType().First().AggregateFrequency.Value == expectedRate)); @@ -100,7 +102,8 @@ namespace osu.Game.Tests.Visual.Gameplay storyboardMods = new[] { testedMod }; }); - createPlayerTest(true); + createPlayerTest(); + skipIntro(); ModTimeRamp gameplayMod = null; @@ -114,17 +117,20 @@ namespace osu.Game.Tests.Visual.Gameplay sound.ChildrenOfType().First().AggregateFrequency.Value == gameplayMod.SpeedChange.Value)); } - private void createPlayerTest(bool skipIntro = false) + private void createPlayerTest() { CreateTest(null); AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null); - AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); - - if (skipIntro) - AddStep("skip intro", () => InputManager.Key(Key.Space)); + waitUntilStoryboardSamplesPlay(); } + private void waitUntilStoryboardSamplesPlay() => AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + + private void allStoryobardSamplesStopped() => AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); + + private void skipIntro() => AddStep("skip intro", () => InputManager.Key(Key.Space)); + private IEnumerable allStoryboardSamples => Player.ChildrenOfType(); protected override bool AllowFail => false; From 2ce4faa3564f13d334e5ed282560488257f19c22 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 3 Mar 2022 00:02:36 +0300 Subject: [PATCH 11/30] Fix typo in method name --- .../Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs index 6b74868944..909cab5e3d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("player paused", () => Player.Pause()); AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value); - allStoryobardSamplesStopped(); + allStoryboardSamplesStopped(); AddStep("player resume", () => Player.Resume()); waitUntilStoryboardSamplesPlay(); @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay createPlayerTest(); skipIntro(); - allStoryobardSamplesStopped(); + allStoryboardSamplesStopped(); waitUntilStoryboardSamplesPlay(); } @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void waitUntilStoryboardSamplesPlay() => AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); - private void allStoryobardSamplesStopped() => AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); + private void allStoryboardSamplesStopped() => AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); private void skipIntro() => AddStep("skip intro", () => InputManager.Key(Key.Space)); From 3630ab2db2da2ee6b0291bf0e3cedfd47a8f020f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 3 Mar 2022 00:09:12 +0300 Subject: [PATCH 12/30] Remove unnecessary nullability of storyboard mods list --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 3 +-- osu.Game/Storyboards/Storyboard.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 840500347f..01e4dfca02 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . 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 System.Threading; @@ -64,7 +63,7 @@ namespace osu.Game.Storyboards.Drawables public DrawableStoryboard(Storyboard storyboard, IReadOnlyList mods) { Storyboard = storyboard; - Mods = mods ?? Array.Empty(); + Mods = mods; Size = new Vector2(640, 480); diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 2faed98ae0..844950336d 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -91,7 +91,7 @@ namespace osu.Game.Storyboards } } - public DrawableStoryboard CreateDrawable(IReadOnlyList mods = null) => + public DrawableStoryboard CreateDrawable(IReadOnlyList mods) => new DrawableStoryboard(this, mods); public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) From 35f532fefa88662a0b536c9dbd4d1341586353be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 17:42:40 +0900 Subject: [PATCH 13/30] Add ability to watch properties via a `RealmAccess` helper method --- osu.Game/Database/RealmAccess.cs | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index fb3052d850..b093c1ed56 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -318,6 +320,66 @@ namespace osu.Game.Database } } + /// + /// Subscribe to the property of a realm object to watch for changes. + /// + /// + /// On subscribing, unless the does not match an object, an initial invocation of will occur immediately. + /// Further invocations will occur when the value changes, but may also fire on a realm recycle with no actual value change. + /// + /// A function to retrieve the relevant model from realm. + /// A function to traverse to the relevant property from the model. + /// A function to be invoked when a change of value occurs. + /// The type of the model. + /// The type of the property to be watched. + /// + /// A subscription token. It must be kept alive for as long as you want to receive change notifications. + /// To stop receiving notifications, call . + /// + public IDisposable SubscribeToPropertyChanged(Func modelAccessor, Expression> propertyLookup, Action onChanged) + where TModel : RealmObjectBase + { + return RegisterCustomSubscription(r => + { + string propertyName = getMemberName(propertyLookup); + + var model = Run(modelAccessor); + var propLookupCompiled = propertyLookup.Compile(); + + if (model == null) + return null; + + model.PropertyChanged += onPropertyChanged; + + // Update initial value immediately. + onChanged(propLookupCompiled(model)); + + return new InvokeOnDisposal(() => model.PropertyChanged -= onPropertyChanged); + + void onPropertyChanged(object sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == propertyName) + onChanged(propLookupCompiled(model)); + } + }); + + static string getMemberName(Expression> expression) + { + if (!(expression is LambdaExpression lambda)) + throw new ArgumentException($"Outermost expression must be a lambda expression", nameof(expression)); + + if (!(lambda.Body is MemberExpression memberExpression)) + throw new ArgumentException($"Lambda body must be a member access expression", nameof(expression)); + + // TODO: nested access can be supported, with more iteration here + // (need to iteratively soft-cast `memberExpression.Expression` into `MemberExpression`s until `lambda.Parameters[0]` is hit) + if (memberExpression.Expression != lambda.Parameters[0]) + throw new ArgumentException($"Nested access expressions are not supported", nameof(expression)); + + return memberExpression.Member.Name; + } + } + /// /// Run work on realm that will be run every time the update thread realm instance gets recycled. /// From cecc746f9e3431ba2b1ae54e89072a262413bda9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 17:42:50 +0900 Subject: [PATCH 14/30] Update existing usages to use `SubscribeToPropertyChanged` --- .../Play/MasterGameplayClockContainer.cs | 24 +++--------------- .../PlayerSettings/BeatmapOffsetControl.cs | 25 ++++++------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 2b6db5f59e..7febec5737 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -86,26 +86,10 @@ namespace osu.Game.Screens.Play userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); - beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => - { - var userSettings = r.Find(beatmap.BeatmapInfo.ID)?.UserSettings; - - if (userSettings == null) // only the case for tests. - return null; - - void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args) - { - if (args.PropertyName == nameof(BeatmapUserSettings.Offset)) - updateOffset(); - } - - updateOffset(); - userSettings.PropertyChanged += onUserSettingsOnPropertyChanged; - - return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged); - - void updateOffset() => userBeatmapOffsetClock.Offset = userSettings.Offset; - }); + beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( + r => r.Find(beatmap.BeatmapInfo.ID)?.UserSettings, + settings => settings.Offset, + val => userBeatmapOffsetClock.Offset = val); // sane default provided by ruleset. startOffset = gameplayStartTime; diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index dc3e80d695..23d7c626da 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -2,13 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; @@ -94,24 +94,13 @@ namespace osu.Game.Screens.Play.PlayerSettings ReferenceScore.BindValueChanged(scoreChanged, true); - beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => - { - var userSettings = r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings; - - if (userSettings == null) // only the case for tests. - return null; - - Current.Value = userSettings.Offset; - userSettings.PropertyChanged += onUserSettingsOnPropertyChanged; - - return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged); - - void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args) + beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( + realm => realm.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, + settings => settings.Offset, + val => { - if (args.PropertyName == nameof(BeatmapUserSettings.Offset)) - Current.Value = userSettings.Offset; - } - }); + Current.Value = val; + }); Current.BindValueChanged(currentChanged); } From 1485a3a28a5ada0a04aedcb50ea9790331064b3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Mar 2022 17:56:49 +0900 Subject: [PATCH 15/30] Add test coverage of proeprty changed subscriptions --- .../RealmSubscriptionRegistrationTests.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 02d617d0e0..363a189f6e 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -47,6 +47,28 @@ namespace osu.Game.Tests.Database void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => lastChanges = changes; } + [Test] + public void TestPropertyChangedSubscription() + { + RunTestWithRealm((realm, _) => + { + bool? receivedValue = null; + + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + + using (realm.SubscribeToPropertyChanged(r => r.All().First(), setInfo => setInfo.Protected, val => receivedValue = val)) + { + Assert.That(receivedValue, Is.False); + + realm.Write(r => r.All().First().Protected = true); + + realm.Run(r => r.Refresh()); + + Assert.That(receivedValue, Is.True); + } + }); + } + [Test] public void TestSubscriptionWithContextLoss() { @@ -163,5 +185,41 @@ namespace osu.Game.Tests.Database Assert.That(beatmapSetInfo, Is.Null); }); } + + [Test] + public void TestPropertyChangedSubscriptionWithContextLoss() + { + RunTestWithRealm((realm, _) => + { + bool? receivedValue = null; + + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + + var subscription = realm.SubscribeToPropertyChanged( + r => r.All().First(), + setInfo => setInfo.Protected, + val => receivedValue = val); + + Assert.That(receivedValue, Is.Not.Null); + receivedValue = null; + + using (realm.BlockAllOperations()) + { + } + + // re-registration after context restore. + realm.Run(r => r.Refresh()); + Assert.That(receivedValue, Is.Not.Null); + + subscription.Dispose(); + receivedValue = null; + + using (realm.BlockAllOperations()) + Assert.That(receivedValue, Is.Null); + + realm.Run(r => r.Refresh()); + Assert.That(receivedValue, Is.Null); + }); + } } } From 0fbc018a4267d55b63b44a1d185fae4f569229d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Mar 2022 20:21:09 +0100 Subject: [PATCH 16/30] Remove redundant string interpolation prefixes --- osu.Game/Database/RealmAccess.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index b093c1ed56..af7c485c57 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -366,15 +366,15 @@ namespace osu.Game.Database static string getMemberName(Expression> expression) { if (!(expression is LambdaExpression lambda)) - throw new ArgumentException($"Outermost expression must be a lambda expression", nameof(expression)); + throw new ArgumentException("Outermost expression must be a lambda expression", nameof(expression)); if (!(lambda.Body is MemberExpression memberExpression)) - throw new ArgumentException($"Lambda body must be a member access expression", nameof(expression)); + throw new ArgumentException("Lambda body must be a member access expression", nameof(expression)); // TODO: nested access can be supported, with more iteration here // (need to iteratively soft-cast `memberExpression.Expression` into `MemberExpression`s until `lambda.Parameters[0]` is hit) if (memberExpression.Expression != lambda.Parameters[0]) - throw new ArgumentException($"Nested access expressions are not supported", nameof(expression)); + throw new ArgumentException("Nested access expressions are not supported", nameof(expression)); return memberExpression.Member.Name; } From edd361d256a2428f9ce0f5a16aa2dc1ceaa2ba96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Mar 2022 20:21:46 +0100 Subject: [PATCH 17/30] Trim unused using directives --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 1 - osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 7febec5737..af58e9d910 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using osu.Framework; using osu.Framework.Allocation; diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 23d7c626da..72c2292624 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Database; From 2e24e7ef56b61320f8516773b2cb5441bc6da3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Mar 2022 20:28:00 +0100 Subject: [PATCH 18/30] Use property expression rather than block --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index 72c2292624..d09e2cae92 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -96,10 +96,7 @@ namespace osu.Game.Screens.Play.PlayerSettings beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( realm => realm.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, settings => settings.Offset, - val => - { - Current.Value = val; - }); + val => Current.Value = val); Current.BindValueChanged(currentChanged); } From 15f65c7897da7f5e3102f00262c2bd8617877135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Mar 2022 20:28:19 +0100 Subject: [PATCH 19/30] Rename lambda param to avoid name shadowing --- osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs index d09e2cae92..c05c5af10d 100644 --- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs +++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs @@ -94,7 +94,7 @@ namespace osu.Game.Screens.Play.PlayerSettings ReferenceScore.BindValueChanged(scoreChanged, true); beatmapOffsetSubscription = realm.SubscribeToPropertyChanged( - realm => realm.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, + r => r.Find(beatmap.Value.BeatmapInfo.ID)?.UserSettings, settings => settings.Offset, val => Current.Value = val); From c38126ba9d708fe8250dbb76c5a8fe91cff464d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Mar 2022 12:05:02 +0900 Subject: [PATCH 20/30] Make mods argument optional for storyboard construction --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 5 +++-- osu.Game/Storyboards/Storyboard.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 01e4dfca02..a0fb7b0b4a 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . 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 System.Threading; @@ -60,10 +61,10 @@ namespace osu.Game.Storyboards.Drawables protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - public DrawableStoryboard(Storyboard storyboard, IReadOnlyList mods) + public DrawableStoryboard(Storyboard storyboard, IReadOnlyList mods = null) { Storyboard = storyboard; - Mods = mods; + Mods = mods ?? Array.Empty(); Size = new Vector2(640, 480); diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 844950336d..2faed98ae0 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -91,7 +91,7 @@ namespace osu.Game.Storyboards } } - public DrawableStoryboard CreateDrawable(IReadOnlyList mods) => + public DrawableStoryboard CreateDrawable(IReadOnlyList mods = null) => new DrawableStoryboard(this, mods); public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) From e1eeb9c6bbaead86f6ce482a69700d6e3ec2144a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Mar 2022 01:43:56 +0100 Subject: [PATCH 21/30] Allow tabbing between textboxes in sample point popover --- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 2 +- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 7 ++++++- .../Edit/Timing/IndeterminateSliderWithTextBoxInput.cs | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 4da8d6a554..fd64cc2056 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -35,7 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => Component.Text = value; } - public Container TabbableContentContainer + public CompositeDrawable TabbableContentContainer { set => Component.TabbableContentContainer = value; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 7d52645aa1..fc0952d4f0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -75,9 +75,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load() { + FillFlowContainer flow; + Children = new Drawable[] { - new FillFlowContainer + flow = new FillFlowContainer { Width = 200, Direction = FillDirection.Vertical, @@ -94,6 +96,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } }; + bank.TabbableContentContainer = flow; + volume.TabbableContentContainer = flow; + // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection. var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); diff --git a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs index e25d83cfb0..0cf2cf6c54 100644 --- a/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/IndeterminateSliderWithTextBoxInput.cs @@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Timing set => slider.KeyboardStep = value; } + public CompositeDrawable TabbableContentContainer + { + set => textBox.TabbableContentContainer = value; + } + private readonly BindableWithCurrent current = new BindableWithCurrent(); public Bindable Current From df0617f34c33a31050c94cd113dd94819d064ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 15:03:21 +0100 Subject: [PATCH 22/30] Implement popup screen title component --- .../TestScenePopupScreenTitle.cs | 44 +++++ .../UserInterface/PopupScreenTitle.cs | 151 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs create mode 100644 osu.Game/Graphics/UserInterface/PopupScreenTitle.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs new file mode 100644 index 0000000000..c214c158a4 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . 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.Game.Graphics.UserInterface; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestScenePopupScreenTitle : OsuTestScene + { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + [Test] + public void TestPopupScreenTitle() + { + AddStep("create content", () => + { + Child = new PopupScreenTitle + { + Title = "Popup Screen Title", + Description = "This is a description.", + Close = () => { } + }; + }); + } + + [Test] + public void TestDisabledExit() + { + AddStep("create content", () => + { + Child = new PopupScreenTitle + { + Title = "Popup Screen Title", + Description = "This is a description." + }; + }); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs b/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs new file mode 100644 index 0000000000..8d5aef7427 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs @@ -0,0 +1,151 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public class PopupScreenTitle : CompositeDrawable + { + public LocalisableString Title + { + get => titleSpriteText.Text; + set => titleSpriteText.Text = value; + } + + public LocalisableString Description + { + get => descriptionSpriteText.Text; + set => descriptionSpriteText.Text = value; + } + + public Action? Close + { + get => closeButton.Action; + set => closeButton.Action = value; + } + + private const float corner_radius = 14; + private const float main_area_height = 70; + + private readonly Container underlayContainer; + private readonly Box underlayBackground; + private readonly Container contentContainer; + private readonly Box contentBackground; + private readonly OsuSpriteText titleSpriteText; + private readonly OsuSpriteText descriptionSpriteText; + private readonly IconButton closeButton; + + public PopupScreenTitle() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Horizontal = 70, + Top = -corner_radius + }, + Children = new Drawable[] + { + underlayContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = main_area_height + 2 * corner_radius, + CornerRadius = corner_radius, + Masking = true, + BorderThickness = 2, + Child = underlayBackground = new Box + { + RelativeSizeAxes = Axes.Both + } + }, + contentContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = main_area_height + corner_radius, + CornerRadius = corner_radius, + Masking = true, + BorderThickness = 2, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Colour4.Black.Opacity(0.1f), + Offset = new Vector2(0, 1), + Radius = 3 + }, + Children = new Drawable[] + { + contentBackground = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Top = corner_radius }, + Padding = new MarginPadding { Horizontal = 100 }, + Children = new Drawable[] + { + titleSpriteText = new OsuSpriteText + { + Font = OsuFont.TorusAlternate.With(size: 20) + }, + descriptionSpriteText = new OsuSpriteText + { + Font = OsuFont.Default.With(size: 12) + } + } + }, + closeButton = new IconButton + { + Icon = FontAwesome.Solid.Times, + Scale = new Vector2(0.6f), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding + { + Right = 21, + Top = corner_radius + } + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + underlayContainer.BorderColour = ColourInfo.GradientVertical(Colour4.Black, colourProvider.Dark4); + underlayBackground.Colour = colourProvider.Dark4; + + contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Dark3, colourProvider.Dark1); + contentBackground.Colour = colourProvider.Dark3; + + closeButton.IconHoverColour = colourProvider.Highlight1; + } + } +} From 54275813b57e88d8709d1fbf1c3e33d329ed7cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Feb 2022 18:10:30 +0100 Subject: [PATCH 23/30] Use text flow container in popup screen title --- .../UserInterface/TestScenePopupScreenTitle.cs | 3 ++- .../Graphics/UserInterface/PopupScreenTitle.cs | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs index c214c158a4..22a8fa8a46 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupScreenTitle.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . 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.Game.Graphics.UserInterface; @@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface Child = new PopupScreenTitle { Title = "Popup Screen Title", - Description = "This is a description.", + Description = string.Join(" ", Enumerable.Repeat("This is a description.", 20)), Close = () => { } }; }); diff --git a/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs b/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs index 8d5aef7427..5b7db09e77 100644 --- a/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs +++ b/osu.Game/Graphics/UserInterface/PopupScreenTitle.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; @@ -22,14 +23,12 @@ namespace osu.Game.Graphics.UserInterface { public LocalisableString Title { - get => titleSpriteText.Text; set => titleSpriteText.Text = value; } public LocalisableString Description { - get => descriptionSpriteText.Text; - set => descriptionSpriteText.Text = value; + set => descriptionText.Text = value; } public Action? Close @@ -46,7 +45,7 @@ namespace osu.Game.Graphics.UserInterface private readonly Container contentContainer; private readonly Box contentBackground; private readonly OsuSpriteText titleSpriteText; - private readonly OsuSpriteText descriptionSpriteText; + private readonly OsuTextFlowContainer descriptionText; private readonly IconButton closeButton; public PopupScreenTitle() @@ -112,9 +111,13 @@ namespace osu.Game.Graphics.UserInterface { Font = OsuFont.TorusAlternate.With(size: 20) }, - descriptionSpriteText = new OsuSpriteText + descriptionText = new OsuTextFlowContainer(t => { - Font = OsuFont.Default.With(size: 12) + t.Font = OsuFont.Default.With(size: 12); + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y } } }, From da29947ecd36fc3479d6e82ee8ca45aba4c1338a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Mar 2022 11:34:06 +0900 Subject: [PATCH 24/30] Disallow interaction with carousel set difficulty icons unless selected I kinda liked this flow, but from multiple reports from users it definitely seems in the way. We can revisit after the new design is applied to song select. Note that this means the tooltips also don't display. If it is preferred that they should (arguable from a UX perspective, since I'd expect to be able to click at that point) then the issue can be addressed using a slightly different path (a few more lines - nothing too complex). --- osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 760915b528..a000cfd5fc 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -16,6 +16,9 @@ namespace osu.Game.Screens.Select.Carousel { public class SetPanelContent : CompositeDrawable { + // Disallow interacting with difficulty icons on a panel until the panel has been selected. + public override bool PropagatePositionalInputSubTree => carouselSet.State.Value == CarouselItemState.Selected; + private readonly CarouselBeatmapSet carouselSet; public SetPanelContent(CarouselBeatmapSet carouselSet) From 2a55c5e02e6ddebe06ece01d067d037fc057a785 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Mar 2022 14:42:59 +0900 Subject: [PATCH 25/30] Add extension method to detect and isolate realm collection-level changes --- .../RealmSubscriptionRegistrationTests.cs | 59 ++++++++++++++++++- osu.Game/Database/RealmExtensions.cs | 11 ++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs index 363a189f6e..d99bcc092d 100644 --- a/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs +++ b/osu.Game.Tests/Database/RealmSubscriptionRegistrationTests.cs @@ -1,25 +1,80 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Tests.Resources; using Realms; -#nullable enable - namespace osu.Game.Tests.Database { [TestFixture] public class RealmSubscriptionRegistrationTests : RealmTest { + [Test] + public void TestSubscriptionCollectionAndPropertyChanges() + { + int collectionChanges = 0; + int propertyChanges = 0; + + ChangeSet? lastChanges = null; + + RunTestWithRealm((realm, _) => + { + var registration = realm.RegisterForNotifications(r => r.All(), onChanged); + + realm.Run(r => r.Refresh()); + + realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo())); + realm.Run(r => r.Refresh()); + + Assert.That(collectionChanges, Is.EqualTo(1)); + Assert.That(propertyChanges, Is.EqualTo(0)); + Assert.That(lastChanges?.InsertedIndices, Has.One.Items); + Assert.That(lastChanges?.ModifiedIndices, Is.Empty); + Assert.That(lastChanges?.NewModifiedIndices, Is.Empty); + + realm.Write(r => r.All().First().Beatmaps.First().CountdownOffset = 5); + realm.Run(r => r.Refresh()); + + Assert.That(collectionChanges, Is.EqualTo(1)); + Assert.That(propertyChanges, Is.EqualTo(1)); + Assert.That(lastChanges?.InsertedIndices, Is.Empty); + Assert.That(lastChanges?.ModifiedIndices, Has.One.Items); + Assert.That(lastChanges?.NewModifiedIndices, Has.One.Items); + + registration.Dispose(); + }); + + void onChanged(IRealmCollection sender, ChangeSet? changes, Exception error) + { + lastChanges = changes; + + if (changes == null) + return; + + if (changes.HasCollectionChanges()) + { + Interlocked.Increment(ref collectionChanges); + } + else + { + Interlocked.Increment(ref propertyChanges); + } + } + } + [Test] public void TestSubscriptionWithAsyncWrite() { diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index e6f3dba39f..551b84f7b6 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -4,6 +4,8 @@ using System; using Realms; +#nullable enable + namespace osu.Game.Database { public static class RealmExtensions @@ -22,5 +24,14 @@ namespace osu.Game.Database transaction.Commit(); return result; } + + /// + /// Whether the provided change set has changes to the top level collection. + /// + /// + /// Realm subscriptions fire on both collection and property changes (including *all* nested properties). + /// Quite often we only care about changes at a collection level. This can be used to guard and early-return when no such changes are in a callback. + /// + public static bool HasCollectionChanges(this ChangeSet changes) => changes.InsertedIndices.Length > 0 || changes.DeletedIndices.Length > 0 || changes.Moves.Length > 0; } } From 622ec531300f0369f37e711936b2dcec56601b72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Mar 2022 14:43:14 +0900 Subject: [PATCH 26/30] Fix `BeatmapLeaderboard` refreshing on unrelated changes to a beatmap --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index eb0addd377..8d1654eb1d 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -191,6 +191,11 @@ namespace osu.Game.Screens.Select.Leaderboards if (cancellationToken.IsCancellationRequested) return; + // This subscription may fire from changes to linked beatmaps, which we don't care about. + // It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications. + if (changes?.HasCollectionChanges() == false) + return; + var scores = sender.AsEnumerable(); if (filterMods && !mods.Value.Any()) From 589a40ca2d48cb14da5e859c3d6669337eb0910c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Mar 2022 17:58:37 +0900 Subject: [PATCH 27/30] Add `EnumMember` naming to `HitResult` to allow for correct json serialisation --- osu.Game/Rulesets/Scoring/HitResult.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index a254f9b760..68a03fa061 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -5,6 +5,7 @@ using System; using System.ComponentModel; using System.Diagnostics; using System.Linq; +using System.Runtime.Serialization; using osu.Framework.Utils; namespace osu.Game.Rulesets.Scoring @@ -16,6 +17,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates that the object has not been judged yet. /// [Description(@"")] + [EnumMember(Value = "none")] [Order(14)] None, @@ -27,32 +29,39 @@ namespace osu.Game.Rulesets.Scoring /// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time). /// [Description(@"Miss")] + [EnumMember(Value = "miss")] [Order(5)] Miss, [Description(@"Meh")] + [EnumMember(Value = "meh")] [Order(4)] Meh, [Description(@"OK")] + [EnumMember(Value = "ok")] [Order(3)] Ok, [Description(@"Good")] + [EnumMember(Value = "good")] [Order(2)] Good, [Description(@"Great")] + [EnumMember(Value = "great")] [Order(1)] Great, [Description(@"Perfect")] + [EnumMember(Value = "perfect")] [Order(0)] Perfect, /// /// Indicates small tick miss. /// + [EnumMember(Value = "small_tick_miss")] [Order(11)] SmallTickMiss, @@ -60,12 +69,14 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a small tick hit. /// [Description(@"S Tick")] + [EnumMember(Value = "small_tick_hit")] [Order(7)] SmallTickHit, /// /// Indicates a large tick miss. /// + [EnumMember(Value = "large_tick_miss")] [Order(10)] LargeTickMiss, @@ -73,6 +84,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a large tick hit. /// [Description(@"L Tick")] + [EnumMember(Value = "large_tick_hit")] [Order(6)] LargeTickHit, @@ -80,6 +92,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a small bonus. /// [Description("S Bonus")] + [EnumMember(Value = "small_bonus")] [Order(9)] SmallBonus, @@ -87,18 +100,21 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a large bonus. /// [Description("L Bonus")] + [EnumMember(Value = "large_bonus")] [Order(8)] LargeBonus, /// /// Indicates a miss that should be ignored for scoring purposes. /// + [EnumMember(Value = "ignore_miss")] [Order(13)] IgnoreMiss, /// /// Indicates a hit that should be ignored for scoring purposes. /// + [EnumMember(Value = "ignore_hit")] [Order(12)] IgnoreHit, } From 9a347af5c79e5f57decfa6be8a420a7f4ef6872e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Mar 2022 17:58:52 +0900 Subject: [PATCH 28/30] Add test coverage of `SubmittableScore` serialisation to (roughly) match spec --- .../TestSubmittableScoreJsonSerialization.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs diff --git a/osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs b/osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs new file mode 100644 index 0000000000..662660bce4 --- /dev/null +++ b/osu.Game.Tests/Online/TestSubmittableScoreJsonSerialization.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Newtonsoft.Json; +using NUnit.Framework; +using osu.Game.IO.Serialization; +using osu.Game.Online.Solo; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Online +{ + /// + /// Basic testing to ensure our attribute-based naming is correctly working. + /// + [TestFixture] + public class TestSubmittableScoreJsonSerialization + { + [Test] + public void TestScoreSerialisationViaExtensionMethod() + { + var score = new SubmittableScore(TestResources.CreateTestScoreInfo()); + + string serialised = score.Serialize(); + + Assert.That(serialised, Contains.Substring("large_tick_hit")); + Assert.That(serialised, Contains.Substring("\"rank\": \"S\"")); + } + + [Test] + public void TestScoreSerialisationWithoutSettings() + { + var score = new SubmittableScore(TestResources.CreateTestScoreInfo()); + + string serialised = JsonConvert.SerializeObject(score); + + Assert.That(serialised, Contains.Substring("large_tick_hit")); + Assert.That(serialised, Contains.Substring("\"rank\":\"S\"")); + } + } +} From a172fc6cb8eb212a17a586279656ceacfac4efd5 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 8 Mar 2022 18:19:11 +0900 Subject: [PATCH 29/30] Add IsBasic() and IsTick() extensions on HitResult --- osu.Game/Rulesets/Scoring/HitResult.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index a254f9b760..ddddf70cc6 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -133,6 +133,30 @@ namespace osu.Game.Rulesets.Scoring public static bool AffectsAccuracy(this HitResult result) => IsScorable(result) && !IsBonus(result); + /// + /// Whether a is a non-tick and non-bonus result. + /// + public static bool IsBasic(this HitResult result) + => IsScorable(result) && !IsTick(result) && !IsBonus(result); + + /// + /// Whether a should be counted as a tick. + /// + public static bool IsTick(this HitResult result) + { + switch (result) + { + case HitResult.LargeTickHit: + case HitResult.LargeTickMiss: + case HitResult.SmallTickHit: + case HitResult.SmallTickMiss: + return true; + + default: + return false; + } + } + /// /// Whether a should be counted as bonus score. /// From b0f40d9e4562898a488cdd3b643553aa7099a4e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Mar 2022 18:38:24 +0900 Subject: [PATCH 30/30] Remove `user` from `SubmittableScore` This wasn't being used by osu-web, and included far too much unnecessary data. Of note, `pp` and `ruleset_id` are also not strictly required, but there's no harm in sending them so I've left them be for now. --- osu.Game/Online/Solo/SubmittableScore.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Online/Solo/SubmittableScore.cs b/osu.Game/Online/Solo/SubmittableScore.cs index 4e4dae5157..9b6da1844a 100644 --- a/osu.Game/Online/Solo/SubmittableScore.cs +++ b/osu.Game/Online/Solo/SubmittableScore.cs @@ -46,9 +46,6 @@ namespace osu.Game.Online.Solo [JsonProperty("mods")] public APIMod[] Mods { get; set; } - [JsonProperty("user")] - public APIUser User { get; set; } - [JsonProperty("statistics")] public Dictionary Statistics { get; set; } @@ -67,7 +64,6 @@ namespace osu.Game.Online.Solo RulesetID = score.RulesetID; Passed = score.Passed; Mods = score.APIMods; - User = score.User; Statistics = score.Statistics; } }