From 1aa36818df11577979863233dd0a149de582504b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 17:47:11 +0900 Subject: [PATCH 01/63] Abstractify GameplayClockContainer --- .../TestSceneSpinnerRotation.cs | 3 +- .../TestSceneGameplayClockContainer.cs | 2 +- .../Gameplay/TestSceneStoryboardSamples.cs | 4 +- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 4 +- .../Screens/Play/GameplayClockContainer.cs | 277 +++++++++--------- osu.Game/Screens/Play/Player.cs | 11 +- osu.Game/Screens/Play/SkipOverlay.cs | 2 +- osu.Game/Screens/Play/SpectatorPlayer.cs | 2 +- 8 files changed, 156 insertions(+), 149 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 14c709cae1..8ff21057b5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Play; using osu.Game.Storyboards; using osu.Game.Tests.Visual; using osuTK; @@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(0); - AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate); + AddStep("adjust track rate", () => ((MasterGameplayClockContainer)Player.GameplayClockContainer).UserPlaybackRate.Value = rate); addSeekStep(1000); AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs index 891537c4ad..4d5dcabbba 100644 --- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Gameplay var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gcc = new GameplayClockContainer(working, 0)); + Add(gcc = new MasterGameplayClockContainer(working, 0)); }); AddStep("start track", () => gcc.Start()); diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index cae5f20332..7394458482 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Gameplay var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gameplayContainer = new GameplayClockContainer(working, 0)); + Add(gameplayContainer = new MasterGameplayClockContainer(working, 0)); gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) { @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Gameplay var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); - Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0) + Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0) { Child = beatmapSkinSourceContainer }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 841722a8f1..e08e03b789 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); working.LoadTrack(); - Child = gameplayClockContainer = new GameplayClockContainer(working, 0) + Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("move mouse", () => InputManager.MoveMouseTo(skip.ScreenSpaceDrawQuad.Centre)); AddStep("click", () => { - increment = skip_time - gameplayClock.CurrentTime - GameplayClockContainer.MINIMUM_SKIP_TIME / 2; + increment = skip_time - gameplayClock.CurrentTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME / 2; InputManager.Click(MouseButton.Left); }); AddStep("click", () => InputManager.Click(MouseButton.Left)); diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ddbb087962..163ed9444f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -19,27 +17,90 @@ using osu.Game.Configuration; namespace osu.Game.Screens.Play { - /// - /// Encapsulates gameplay timing logic and provides a for children. - /// - public class GameplayClockContainer : Container + public abstract class GameplayClockContainer : Container { - private readonly WorkingBeatmap beatmap; - - [NotNull] - private ITrack track; + /// + /// The final clock which is exposed to underlying components. + /// + public GameplayClock GameplayClock { get; private set; } public readonly BindableBool IsPaused = new BindableBool(); /// /// The decoupled clock used for gameplay. Should be used for seeks and clock control. /// - private readonly DecoupleableInterpolatingFramedClock adjustableClock; + protected readonly DecoupleableInterpolatingFramedClock AdjustableClock; - private readonly double gameplayStartTime; - private readonly bool startAtGameplayStart; + protected readonly IClock SourceClock; - private readonly double firstHitObjectTime; + protected GameplayClockContainer(IClock sourceClock) + { + SourceClock = sourceClock; + + RelativeSizeAxes = Axes.Both; + + AdjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + AdjustableClock.ChangeSource(SourceClock); + + IsPaused.BindValueChanged(OnPauseChanged); + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableClock)); + + return dependencies; + } + + public virtual void Start() + { + if (!AdjustableClock.IsRunning) + { + // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time + // This accounts for the clock source potentially taking time to enter a completely stopped state + Seek(GameplayClock.CurrentTime); + + AdjustableClock.Start(); + } + + IsPaused.Value = false; + } + + /// + /// Seek to a specific time in gameplay. + /// + /// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track). + /// + /// + /// The destination time to seek to. + public virtual void Seek(double time) => AdjustableClock.Seek(time); + + public virtual void Stop() => IsPaused.Value = true; + + public virtual void Restart() + { + AdjustableClock.Seek(0); + AdjustableClock.Stop(); + + if (!IsPaused.Value) + Start(); + } + + protected abstract void OnPauseChanged(ValueChangedEvent isPaused); + + protected abstract GameplayClock CreateGameplayClock(IClock source); + } + + public class MasterGameplayClockContainer : GameplayClockContainer + { + /// + /// Duration before gameplay start time required before skip button displays. + /// + public const double MINIMUM_SKIP_TIME = 1000; + + protected new DecoupleableInterpolatingFramedClock SourceClock => (DecoupleableInterpolatingFramedClock)base.SourceClock; public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { @@ -49,73 +110,32 @@ namespace osu.Game.Screens.Play Precision = 0.1, }; - /// - /// The final clock which is exposed to underlying components. - /// - public GameplayClock GameplayClock => localGameplayClock; + private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; - [Cached(typeof(GameplayClock))] - private readonly LocalGameplayClock localGameplayClock; + private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); + private readonly WorkingBeatmap beatmap; + private readonly double gameplayStartTime; + private readonly bool startAtGameplayStart; + private readonly double firstHitObjectTime; + + private FramedOffsetClock userOffsetClock; + private FramedOffsetClock platformOffsetClock; + private LocalGameplayClock localGameplayClock; private Bindable userAudioOffset; - private readonly FramedOffsetClock userOffsetClock; - - private readonly FramedOffsetClock platformOffsetClock; - - /// - /// Creates a new . - /// - /// The beatmap being played. - /// The suggested time to start gameplay at. - /// - /// Whether should be used regardless of when storyboard events and hitobjects are supposed to start. - /// - public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) + public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) + : base(new DecoupleableInterpolatingFramedClock()) { this.beatmap = beatmap; this.gameplayStartTime = gameplayStartTime; this.startAtGameplayStart = startAtGameplayStart; - track = beatmap.Track; firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - RelativeSizeAxes = Axes.Both; - - adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - - // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new HardwareCorrectionOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - - // the final usable gameplay clock with user-set offsets applied. - userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); - - // the clock to be exposed via DI to children. - localGameplayClock = new LocalGameplayClock(userOffsetClock); - - GameplayClock.IsPaused.BindTo(IsPaused); - - IsPaused.BindValueChanged(onPauseChanged); + SourceClock.ChangeSource(beatmap.Track); } - private void onPauseChanged(ValueChangedEvent isPaused) - { - if (isPaused.NewValue) - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop()); - else - this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); - } - - private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; - - /// - /// Duration before gameplay start time required before skip button displays. - /// - public const double MINIMUM_SKIP_TIME = 1000; - - private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); - [BackgroundDependencyLoader] private void load(OsuConfigManager config) { @@ -143,39 +163,31 @@ namespace osu.Game.Screens.Play Seek(startTime); - adjustableClock.ProcessFrame(); + AdjustableClock.ProcessFrame(); } - public void Restart() + protected override void OnPauseChanged(ValueChangedEvent isPaused) { - Task.Run(() => - { - track.Seek(0); - track.Stop(); - - Schedule(() => - { - adjustableClock.ChangeSource(track); - updateRate(); - - if (!IsPaused.Value) - Start(); - }); - }); + if (isPaused.NewValue) + this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableClock.Stop()); + else + this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); } - public void Start() + public override void Seek(double time) { - if (!adjustableClock.IsRunning) - { - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the audio clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); + // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. + // we may want to consider reversing the application of offsets in the future as it may feel more correct. + base.Seek(time - totalOffset); - adjustableClock.Start(); - } + // manually process frame to ensure GameplayClock is correctly updated after a seek. + userOffsetClock.ProcessFrame(); + } - IsPaused.Value = false; + public override void Restart() + { + updateRate(); + base.Restart(); } /// @@ -195,26 +207,24 @@ namespace osu.Game.Screens.Play Seek(skipTarget); } - /// - /// Seek to a specific time in gameplay. - /// - /// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track). - /// - /// - /// The destination time to seek to. - public void Seek(double time) + protected override GameplayClock CreateGameplayClock(IClock source) { - // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. - // we may want to consider reversing the application of offsets in the future as it may feel more correct. - adjustableClock.Seek(time - totalOffset); + // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. + // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + platformOffsetClock = new HardwareCorrectionOffsetClock(source) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - // manually process frame to ensure GameplayClock is correctly updated after a seek. - userOffsetClock.ProcessFrame(); + // the final usable gameplay clock with user-set offsets applied. + userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); + + return localGameplayClock = new LocalGameplayClock(userOffsetClock); } - public void Stop() + protected override void Update() { - IsPaused.Value = true; + if (!IsPaused.Value) + userOffsetClock.ProcessFrame(); + + base.Update(); } /// @@ -223,19 +233,7 @@ namespace osu.Game.Screens.Play public void StopUsingBeatmapClock() { removeSourceClockAdjustments(); - - track = new TrackVirtual(track.Length); - adjustableClock.ChangeSource(track); - } - - protected override void Update() - { - if (!IsPaused.Value) - { - userOffsetClock.ProcessFrame(); - } - - base.Update(); + SourceClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); } private bool speedAdjustmentsApplied; @@ -245,6 +243,8 @@ namespace osu.Game.Screens.Play if (speedAdjustmentsApplied) return; + var track = (Track)SourceClock.Source; + track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); @@ -254,15 +254,12 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = true; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - removeSourceClockAdjustments(); - } - private void removeSourceClockAdjustments() { - if (!speedAdjustmentsApplied) return; + if (!speedAdjustmentsApplied) + return; + + var track = (Track)SourceClock.Source; track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); @@ -273,16 +270,10 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = false; } - private class LocalGameplayClock : GameplayClock + protected override void Dispose(bool isDisposing) { - public readonly List> MutableNonGameplayAdjustments = new List>(); - - public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; - - public LocalGameplayClock(FramedOffsetClock underlyingClock) - : base(underlyingClock) - { - } + base.Dispose(isDisposing); + removeSourceClockAdjustments(); } private class HardwareCorrectionOffsetClock : FramedOffsetClock @@ -296,5 +287,17 @@ namespace osu.Game.Screens.Play { } } + + private class LocalGameplayClock : GameplayClock + { + public readonly List> MutableNonGameplayAdjustments = new List>(); + + public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + + public LocalGameplayClock(FramedOffsetClock underlyingClock) + : base(underlyingClock) + { + } + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index efe5d26409..e07c40e8ff 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -295,7 +295,7 @@ namespace osu.Game.Screens.Play IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } - protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new GameplayClockContainer(beatmap, gameplayStart); + protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); private Drawable createUnderlayComponents() => DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; @@ -342,7 +342,6 @@ namespace osu.Game.Screens.Play Action = () => PerformExit(true), IsPaused = { BindTarget = GameplayClockContainer.IsPaused } }, - PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, KeyCounter = { AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, @@ -386,6 +385,9 @@ namespace osu.Game.Screens.Play } }; + if (GameplayClockContainer is MasterGameplayClockContainer master) + HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate; + if (!Configuration.AllowSkippingIntro) skipOverlay.Expire(); @@ -533,7 +535,8 @@ namespace osu.Game.Screens.Play // user requested skip // disable sample playback to stop currently playing samples and perform skip samplePlaybackDisabled.Value = true; - GameplayClockContainer.Skip(); + + (GameplayClockContainer as MasterGameplayClockContainer)?.Skip(); // return samplePlaybackDisabled.Value to what is defined by the beatmap's current state updateSampleDisabledState(); @@ -832,7 +835,7 @@ namespace osu.Game.Screens.Play // GameplayClockContainer performs seeks / start / stop operations on the beatmap's track. // as we are no longer the current screen, we cannot guarantee the track is still usable. - GameplayClockContainer?.StopUsingBeatmapClock(); + (GameplayClockContainer as MasterGameplayClockContainer)?.StopUsingBeatmapClock(); musicController.ResetTrackAdjustments(); diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 3f214e49d9..ddb78dfb67 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -90,7 +90,7 @@ namespace osu.Game.Screens.Play private const double fade_time = 300; - private double fadeOutBeginTime => startTime - GameplayClockContainer.MINIMUM_SKIP_TIME; + private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME; protected override void LoadComplete() { diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index fdf996150f..9822f62dd8 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Play if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000) return base.CreateGameplayClockContainer(beatmap, gameplayStart); - return new GameplayClockContainer(beatmap, firstFrameTime.Value, true); + return new MasterGameplayClockContainer(beatmap, firstFrameTime.Value, true); } public override bool OnExiting(IScreen next) From 2935f87e7082bd603c9a09aecda6ba51628a184b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 18:29:34 +0900 Subject: [PATCH 02/63] Fix IsPaused not being bound --- osu.Game/Screens/Play/GameplayClockContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 163ed9444f..a97a87d73f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -50,6 +50,7 @@ namespace osu.Game.Screens.Play var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableClock)); + GameplayClock.IsPaused.BindTo(IsPaused); return dependencies; } From b53b30c1a97a71822359016e87d1334ce8097476 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 19:32:48 +0900 Subject: [PATCH 03/63] Fix incorrect offset due to another intermediate Decoupleable clock --- .../Screens/Play/GameplayClockContainer.cs | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index a97a87d73f..36ca7415d0 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -31,16 +31,12 @@ namespace osu.Game.Screens.Play /// protected readonly DecoupleableInterpolatingFramedClock AdjustableClock; - protected readonly IClock SourceClock; - protected GameplayClockContainer(IClock sourceClock) { - SourceClock = sourceClock; - RelativeSizeAxes = Axes.Both; AdjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - AdjustableClock.ChangeSource(SourceClock); + AdjustableClock.ChangeSource(sourceClock); IsPaused.BindValueChanged(OnPauseChanged); } @@ -101,7 +97,7 @@ namespace osu.Game.Screens.Play /// public const double MINIMUM_SKIP_TIME = 1000; - protected new DecoupleableInterpolatingFramedClock SourceClock => (DecoupleableInterpolatingFramedClock)base.SourceClock; + protected Track Track => (Track)AdjustableClock.Source; public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { @@ -126,15 +122,13 @@ namespace osu.Game.Screens.Play private Bindable userAudioOffset; public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) - : base(new DecoupleableInterpolatingFramedClock()) + : base(beatmap.Track) { this.beatmap = beatmap; this.gameplayStartTime = gameplayStartTime; this.startAtGameplayStart = startAtGameplayStart; firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - - SourceClock.ChangeSource(beatmap.Track); } [BackgroundDependencyLoader] @@ -234,7 +228,7 @@ namespace osu.Game.Screens.Play public void StopUsingBeatmapClock() { removeSourceClockAdjustments(); - SourceClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); + AdjustableClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); } private bool speedAdjustmentsApplied; @@ -244,10 +238,8 @@ namespace osu.Game.Screens.Play if (speedAdjustmentsApplied) return; - var track = (Track)SourceClock.Source; - - track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); @@ -260,10 +252,8 @@ namespace osu.Game.Screens.Play if (!speedAdjustmentsApplied) return; - var track = (Track)SourceClock.Source; - - track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); From 18c69cdaf7bb536d22c704171ba9bd03c00689cd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 19:50:22 +0900 Subject: [PATCH 04/63] Split out files --- .../Screens/Play/GameplayClockContainer.cs | 214 +---------------- .../Play/MasterGameplayClockContainer.cs | 220 ++++++++++++++++++ 2 files changed, 222 insertions(+), 212 deletions(-) create mode 100644 osu.Game/Screens/Play/MasterGameplayClockContainer.cs diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 36ca7415d0..d8e6fda87e 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -1,19 +1,11 @@ // 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 osu.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; -using osu.Game.Beatmaps; -using osu.Game.Configuration; namespace osu.Game.Screens.Play { @@ -38,7 +30,7 @@ namespace osu.Game.Screens.Play AdjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; AdjustableClock.ChangeSource(sourceClock); - IsPaused.BindValueChanged(OnPauseChanged); + IsPaused.BindValueChanged(OnIsPausedChanged); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -85,210 +77,8 @@ namespace osu.Game.Screens.Play Start(); } - protected abstract void OnPauseChanged(ValueChangedEvent isPaused); + protected abstract void OnIsPausedChanged(ValueChangedEvent isPaused); protected abstract GameplayClock CreateGameplayClock(IClock source); } - - public class MasterGameplayClockContainer : GameplayClockContainer - { - /// - /// Duration before gameplay start time required before skip button displays. - /// - public const double MINIMUM_SKIP_TIME = 1000; - - protected Track Track => (Track)AdjustableClock.Source; - - public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) - { - Default = 1, - MinValue = 0.5, - MaxValue = 2, - Precision = 0.1, - }; - - private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; - - private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); - - private readonly WorkingBeatmap beatmap; - private readonly double gameplayStartTime; - private readonly bool startAtGameplayStart; - private readonly double firstHitObjectTime; - - private FramedOffsetClock userOffsetClock; - private FramedOffsetClock platformOffsetClock; - private LocalGameplayClock localGameplayClock; - private Bindable userAudioOffset; - - public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) - : base(beatmap.Track) - { - this.beatmap = beatmap; - this.gameplayStartTime = gameplayStartTime; - this.startAtGameplayStart = startAtGameplayStart; - - firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); - userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); - - // sane default provided by ruleset. - double startTime = gameplayStartTime; - - if (!startAtGameplayStart) - { - startTime = Math.Min(0, startTime); - - // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. - // this is commonly used to display an intro before the audio track start. - double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime; - if (firstStoryboardEvent != null) - startTime = Math.Min(startTime, firstStoryboardEvent.Value); - - // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. - // this is not available as an option in the live editor but can still be applied via .osu editing. - if (beatmap.BeatmapInfo.AudioLeadIn > 0) - startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); - } - - Seek(startTime); - - AdjustableClock.ProcessFrame(); - } - - protected override void OnPauseChanged(ValueChangedEvent isPaused) - { - if (isPaused.NewValue) - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableClock.Stop()); - else - this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); - } - - public override void Seek(double time) - { - // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. - // we may want to consider reversing the application of offsets in the future as it may feel more correct. - base.Seek(time - totalOffset); - - // manually process frame to ensure GameplayClock is correctly updated after a seek. - userOffsetClock.ProcessFrame(); - } - - public override void Restart() - { - updateRate(); - base.Restart(); - } - - /// - /// Skip forward to the next valid skip point. - /// - public void Skip() - { - if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME) - return; - - double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME; - - if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) - // double skip exception for storyboards with very long intros - skipTarget = 0; - - Seek(skipTarget); - } - - protected override GameplayClock CreateGameplayClock(IClock source) - { - // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. - // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new HardwareCorrectionOffsetClock(source) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; - - // the final usable gameplay clock with user-set offsets applied. - userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); - - return localGameplayClock = new LocalGameplayClock(userOffsetClock); - } - - protected override void Update() - { - if (!IsPaused.Value) - userOffsetClock.ProcessFrame(); - - base.Update(); - } - - /// - /// Changes the backing clock to avoid using the originally provided track. - /// - public void StopUsingBeatmapClock() - { - removeSourceClockAdjustments(); - AdjustableClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); - } - - private bool speedAdjustmentsApplied; - - private void updateRate() - { - if (speedAdjustmentsApplied) - return; - - Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - - localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); - localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); - - speedAdjustmentsApplied = true; - } - - private void removeSourceClockAdjustments() - { - if (!speedAdjustmentsApplied) - return; - - Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - - localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); - localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); - - speedAdjustmentsApplied = false; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - removeSourceClockAdjustments(); - } - - private class HardwareCorrectionOffsetClock : FramedOffsetClock - { - // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. - // base implementation already adds offset at 1.0 rate, so we only add the difference from that here. - public override double CurrentTime => base.CurrentTime + Offset * (Rate - 1); - - public HardwareCorrectionOffsetClock(IClock source, bool processSource = true) - : base(source, processSource) - { - } - } - - private class LocalGameplayClock : GameplayClock - { - public readonly List> MutableNonGameplayAdjustments = new List>(); - - public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; - - public LocalGameplayClock(FramedOffsetClock underlyingClock) - : base(underlyingClock) - { - } - } - } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs new file mode 100644 index 0000000000..efc8ca732e --- /dev/null +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -0,0 +1,220 @@ +// 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 osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; + +namespace osu.Game.Screens.Play +{ + public class MasterGameplayClockContainer : GameplayClockContainer + { + /// + /// Duration before gameplay start time required before skip button displays. + /// + public const double MINIMUM_SKIP_TIME = 1000; + + protected Track Track => (Track)AdjustableClock.Source; + + public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) + { + Default = 1, + MinValue = 0.5, + MaxValue = 2, + Precision = 0.1, + }; + + private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; + + private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); + + private readonly WorkingBeatmap beatmap; + private readonly double gameplayStartTime; + private readonly bool startAtGameplayStart; + private readonly double firstHitObjectTime; + + private FramedOffsetClock userOffsetClock; + private FramedOffsetClock platformOffsetClock; + private LocalGameplayClock localGameplayClock; + private Bindable userAudioOffset; + + public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) + : base(beatmap.Track) + { + this.beatmap = beatmap; + this.gameplayStartTime = gameplayStartTime; + this.startAtGameplayStart = startAtGameplayStart; + + firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); + userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); + + // sane default provided by ruleset. + double startTime = gameplayStartTime; + + if (!startAtGameplayStart) + { + startTime = Math.Min(0, startTime); + + // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. + // this is commonly used to display an intro before the audio track start. + double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime; + if (firstStoryboardEvent != null) + startTime = Math.Min(startTime, firstStoryboardEvent.Value); + + // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. + // this is not available as an option in the live editor but can still be applied via .osu editing. + if (beatmap.BeatmapInfo.AudioLeadIn > 0) + startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); + } + + Seek(startTime); + + AdjustableClock.ProcessFrame(); + } + + protected override void OnIsPausedChanged(ValueChangedEvent isPaused) + { + if (isPaused.NewValue) + this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableClock.Stop()); + else + this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); + } + + public override void Seek(double time) + { + // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. + // we may want to consider reversing the application of offsets in the future as it may feel more correct. + base.Seek(time - totalOffset); + + // manually process frame to ensure GameplayClock is correctly updated after a seek. + userOffsetClock.ProcessFrame(); + } + + public override void Restart() + { + updateRate(); + base.Restart(); + } + + /// + /// Skip forward to the next valid skip point. + /// + public void Skip() + { + if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME) + return; + + double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME; + + if (GameplayClock.CurrentTime < 0 && skipTarget > 6000) + // double skip exception for storyboards with very long intros + skipTarget = 0; + + Seek(skipTarget); + } + + protected override GameplayClock CreateGameplayClock(IClock source) + { + // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. + // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + platformOffsetClock = new HardwareCorrectionOffsetClock(source) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + + // the final usable gameplay clock with user-set offsets applied. + userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); + + return localGameplayClock = new LocalGameplayClock(userOffsetClock); + } + + protected override void Update() + { + if (!IsPaused.Value) + userOffsetClock.ProcessFrame(); + + base.Update(); + } + + /// + /// Changes the backing clock to avoid using the originally provided track. + /// + public void StopUsingBeatmapClock() + { + removeSourceClockAdjustments(); + AdjustableClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); + } + + private bool speedAdjustmentsApplied; + + private void updateRate() + { + if (speedAdjustmentsApplied) + return; + + Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + + localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); + localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); + + speedAdjustmentsApplied = true; + } + + private void removeSourceClockAdjustments() + { + if (!speedAdjustmentsApplied) + return; + + Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + + localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); + localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); + + speedAdjustmentsApplied = false; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + removeSourceClockAdjustments(); + } + + private class HardwareCorrectionOffsetClock : FramedOffsetClock + { + // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. + // base implementation already adds offset at 1.0 rate, so we only add the difference from that here. + public override double CurrentTime => base.CurrentTime + Offset * (Rate - 1); + + public HardwareCorrectionOffsetClock(IClock source, bool processSource = true) + : base(source, processSource) + { + } + } + + private class LocalGameplayClock : GameplayClock + { + public readonly List> MutableNonGameplayAdjustments = new List>(); + + public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + + public LocalGameplayClock(FramedOffsetClock underlyingClock) + : base(underlyingClock) + { + } + } + } +} From f56125bd682b68df3c3a6a20e3c24b6d758486ee Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Apr 2021 21:15:14 +0900 Subject: [PATCH 05/63] Update clock from base class --- osu.Game/Screens/Play/GameplayClockContainer.cs | 12 +++++++++++- .../Screens/Play/MasterGameplayClockContainer.cs | 12 +++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index d8e6fda87e..6d863f0094 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -77,8 +77,18 @@ namespace osu.Game.Screens.Play Start(); } + protected override void Update() + { + if (!IsPaused.Value) + ClockToProcess.ProcessFrame(); + + base.Update(); + } + protected abstract void OnIsPausedChanged(ValueChangedEvent isPaused); - protected abstract GameplayClock CreateGameplayClock(IClock source); + protected virtual IFrameBasedClock ClockToProcess => AdjustableClock; + + protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source); } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index efc8ca732e..83e21f3c1d 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -128,7 +128,9 @@ namespace osu.Game.Screens.Play Seek(skipTarget); } - protected override GameplayClock CreateGameplayClock(IClock source) + protected override IFrameBasedClock ClockToProcess => userOffsetClock; + + protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) { // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. @@ -140,14 +142,6 @@ namespace osu.Game.Screens.Play return localGameplayClock = new LocalGameplayClock(userOffsetClock); } - protected override void Update() - { - if (!IsPaused.Value) - userOffsetClock.ProcessFrame(); - - base.Update(); - } - /// /// Changes the backing clock to avoid using the originally provided track. /// From 5652490d61c4ee2c5b046e8baf7e5524303a1a31 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 14:11:55 +0900 Subject: [PATCH 06/63] Fix OnUserBeganPlaying not being invoked if already watching --- .../Visual/Gameplay/TestSceneSpectator.cs | 24 ++++++++++++++++ .../Spectator/SpectatorStreamingClient.cs | 28 +++++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 397b37718d..ea66144b21 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.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; @@ -204,6 +205,29 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator); } + [Test] + public void OnUserBeganPlayingCallbackInvokedOnNewAdd() + { + bool callbackInvoked = false; + Action callbackAction = (_, __) => callbackInvoked = true; + + AddStep("bind first event", () => testSpectatorStreamingClient.OnUserBeganPlaying += callbackAction); + start(); + AddAssert("callback invoked", () => callbackInvoked); + + AddStep("reset", () => + { + testSpectatorStreamingClient.OnUserBeganPlaying -= callbackAction; + callbackInvoked = false; + }); + + AddStep("bind event again", () => testSpectatorStreamingClient.OnUserBeganPlaying += callbackAction); + AddAssert("callback invoked", () => callbackInvoked); + + // Don't leave the event bound if test run succeeded. + AddStep("reset", () => testSpectatorStreamingClient.OnUserBeganPlaying -= callbackAction); + } + private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 3a586874fe..7bea49e102 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -60,6 +60,7 @@ namespace osu.Game.Online.Spectator private IBindable> currentMods { get; set; } private readonly SpectatorState currentState = new SpectatorState(); + private readonly Dictionary currentUserStates = new Dictionary(); private bool isPlaying; @@ -68,10 +69,25 @@ namespace osu.Game.Online.Spectator /// public event Action OnNewFrames; + private event Action onUserBeganPlaying; + /// - /// Called whenever a user starts a play session. + /// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session. /// - public event Action OnUserBeganPlaying; + public event Action OnUserBeganPlaying + { + add + { + onUserBeganPlaying += value; + + lock (userLock) + { + foreach (var (userId, state) in currentUserStates) + value?.Invoke(userId, state); + } + } + remove => onUserBeganPlaying -= value; + } /// /// Called whenever a user finishes a play session. @@ -134,7 +150,10 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); - OnUserBeganPlaying?.Invoke(userId, state); + lock (userLock) + currentUserStates[userId] = state; + + onUserBeganPlaying?.Invoke(userId, state); return Task.CompletedTask; } @@ -143,6 +162,9 @@ namespace osu.Game.Online.Spectator { playingUsers.Remove(userId); + lock (userLock) + currentUserStates.Remove(userId); + OnUserFinishedPlaying?.Invoke(userId, state); return Task.CompletedTask; From ca74f413cd10ccaee2dd1e51c5190019262b76c8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 17:29:42 +0900 Subject: [PATCH 07/63] Change to explicit method instead --- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Spectator/SpectatorStreamingClient.cs | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index ea66144b21..def662d3ea 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -221,7 +221,7 @@ namespace osu.Game.Tests.Visual.Gameplay callbackInvoked = false; }); - AddStep("bind event again", () => testSpectatorStreamingClient.OnUserBeganPlaying += callbackAction); + AddStep("bind event with run once immediately", () => testSpectatorStreamingClient.BindUserBeganPlaying(callbackAction, true)); AddAssert("callback invoked", () => callbackInvoked); // Don't leave the event bound if test run succeeded. diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 7bea49e102..4bbc420223 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -69,25 +69,10 @@ namespace osu.Game.Online.Spectator /// public event Action OnNewFrames; - private event Action onUserBeganPlaying; - /// /// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session. /// - public event Action OnUserBeganPlaying - { - add - { - onUserBeganPlaying += value; - - lock (userLock) - { - foreach (var (userId, state) in currentUserStates) - value?.Invoke(userId, state); - } - } - remove => onUserBeganPlaying -= value; - } + public event Action OnUserBeganPlaying; /// /// Called whenever a user finishes a play session. @@ -153,7 +138,7 @@ namespace osu.Game.Online.Spectator lock (userLock) currentUserStates[userId] = state; - onUserBeganPlaying?.Invoke(userId, state); + OnUserBeganPlaying?.Invoke(userId, state); return Task.CompletedTask; } @@ -290,5 +275,24 @@ namespace osu.Game.Online.Spectator lastSendTime = Time.Current; } + + /// + /// Bind an action to with the option of running the bound action once immediately. + /// + /// The action to perform when a user begins playing. + /// Whether the action provided in should be run once immediately for all users currently playing. + public void BindUserBeganPlaying(Action callback, bool runOnceImmediately = false) + { + OnUserBeganPlaying += callback; + + if (!runOnceImmediately) + return; + + lock (userLock) + { + foreach (var (userId, state) in currentUserStates) + callback(userId, state); + } + } } } From 377e5ce6b396c585a0652cf63253966c59c06948 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 18:21:35 +0900 Subject: [PATCH 08/63] Fix test incorrect sending state too often --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index def662d3ea..392419649b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -312,7 +312,7 @@ namespace osu.Game.Tests.Visual.Gameplay public override void WatchUser(int userId) { - if (sentState) + if (!PlayingUsers.Contains(userId) && sentState) { // usually the server would do this. sendState(beatmapId); From 46d2181d42930ac61f6da22f8146ba2b74357929 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 18:21:56 +0900 Subject: [PATCH 09/63] Remove now unnecessary (duplicating) test --- .../Visual/Gameplay/TestSceneSpectator.cs | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 392419649b..74ce66096e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.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; @@ -205,29 +204,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("screen didn't change", () => Stack.CurrentScreen is SoloSpectator); } - [Test] - public void OnUserBeganPlayingCallbackInvokedOnNewAdd() - { - bool callbackInvoked = false; - Action callbackAction = (_, __) => callbackInvoked = true; - - AddStep("bind first event", () => testSpectatorStreamingClient.OnUserBeganPlaying += callbackAction); - start(); - AddAssert("callback invoked", () => callbackInvoked); - - AddStep("reset", () => - { - testSpectatorStreamingClient.OnUserBeganPlaying -= callbackAction; - callbackInvoked = false; - }); - - AddStep("bind event with run once immediately", () => testSpectatorStreamingClient.BindUserBeganPlaying(callbackAction, true)); - AddAssert("callback invoked", () => callbackInvoked); - - // Don't leave the event bound if test run succeeded. - AddStep("reset", () => testSpectatorStreamingClient.OnUserBeganPlaying -= callbackAction); - } - private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; From 274e33184b8afb9d10ef8b2d380edc8b742ee2ab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 18:22:22 +0900 Subject: [PATCH 10/63] Fix SpectatorScreen potentially missing user playing callbacks --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 42 ++++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 6dd3144fc8..7be6c6183b 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -62,26 +62,42 @@ namespace osu.Game.Screens.Spectate { base.LoadComplete(); - spectatorClient.OnUserBeganPlaying += userBeganPlaying; - spectatorClient.OnUserFinishedPlaying += userFinishedPlaying; - spectatorClient.OnNewFrames += userSentFrames; - - foreach (var id in userIds) + populateAllUsers().ContinueWith(_ => Schedule(() => { - userLookupCache.GetUserAsync(id).ContinueWith(u => Schedule(() => + spectatorClient.BindUserBeganPlaying(userBeganPlaying, true); + spectatorClient.OnUserFinishedPlaying += userFinishedPlaying; + spectatorClient.OnNewFrames += userSentFrames; + + managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); + + lock (stateLock) { - if (u.Result == null) + foreach (var (id, _) in userMap) + spectatorClient.WatchUser(id); + } + })); + } + + private Task populateAllUsers() + { + var userLookupTasks = new Task[userIds.Length]; + + for (int i = 0; i < userIds.Length; i++) + { + var userId = userIds[i]; + + userLookupTasks[i] = userLookupCache.GetUserAsync(userId).ContinueWith(task => + { + if (!task.IsCompletedSuccessfully) return; lock (stateLock) - userMap[id] = u.Result; - - spectatorClient.WatchUser(id); - }), TaskContinuationOptions.OnlyOnRanToCompletion); + userMap[userId] = task.Result; + }); } - managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); - managerUpdated.BindValueChanged(beatmapUpdated); + return Task.WhenAll(userLookupTasks); } private void beatmapUpdated(ValueChangedEvent> e) From 6301111fa3c1d274c68766d3c1456ebd74d95138 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 20:15:42 +0900 Subject: [PATCH 11/63] Remove ClockToProcess, always process underlying clock --- osu.Game/Screens/Play/GameplayClock.cs | 18 +++++++++--------- .../Screens/Play/GameplayClockContainer.cs | 4 +--- .../Play/MasterGameplayClockContainer.cs | 2 -- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index db4b5d300b..54aa395f5f 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Play /// public class GameplayClock : IFrameBasedClock { - private readonly IFrameBasedClock underlyingClock; + internal readonly IFrameBasedClock UnderlyingClock; public readonly BindableBool IsPaused = new BindableBool(); @@ -30,12 +30,12 @@ namespace osu.Game.Screens.Play public GameplayClock(IFrameBasedClock underlyingClock) { - this.underlyingClock = underlyingClock; + UnderlyingClock = underlyingClock; } - public double CurrentTime => underlyingClock.CurrentTime; + public double CurrentTime => UnderlyingClock.CurrentTime; - public double Rate => underlyingClock.Rate; + public double Rate => UnderlyingClock.Rate; /// /// The rate of gameplay when playback is at 100%. @@ -59,19 +59,19 @@ namespace osu.Game.Screens.Play } } - public bool IsRunning => underlyingClock.IsRunning; + public bool IsRunning => UnderlyingClock.IsRunning; public void ProcessFrame() { // intentionally not updating the underlying clock (handled externally). } - public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; + public double ElapsedFrameTime => UnderlyingClock.ElapsedFrameTime; - public double FramesPerSecond => underlyingClock.FramesPerSecond; + public double FramesPerSecond => UnderlyingClock.FramesPerSecond; - public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; + public FrameTimeInfo TimeInfo => UnderlyingClock.TimeInfo; - public IClock Source => underlyingClock; + public IClock Source => UnderlyingClock; } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6d863f0094..b7dc55277f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -80,15 +80,13 @@ namespace osu.Game.Screens.Play protected override void Update() { if (!IsPaused.Value) - ClockToProcess.ProcessFrame(); + GameplayClock.UnderlyingClock.ProcessFrame(); base.Update(); } protected abstract void OnIsPausedChanged(ValueChangedEvent isPaused); - protected virtual IFrameBasedClock ClockToProcess => AdjustableClock; - protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source); } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 83e21f3c1d..5eb82bf0fa 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -128,8 +128,6 @@ namespace osu.Game.Screens.Play Seek(skipTarget); } - protected override IFrameBasedClock ClockToProcess => userOffsetClock; - protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) { // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. From 3a78c19f9695ad5519e8494e8e888f25d50998b9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 20:33:29 +0900 Subject: [PATCH 12/63] More refactoring/xmldocs --- .../Screens/Play/GameplayClockContainer.cs | 64 ++++++++++++++----- .../Play/MasterGameplayClockContainer.cs | 16 +++-- 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index b7dc55277f..642ede5f1c 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -9,26 +9,36 @@ using osu.Framework.Timing; namespace osu.Game.Screens.Play { + /// + /// Encapsulates gameplay timing logic and provides a via DI for gameplay components to use. + /// public abstract class GameplayClockContainer : Container { /// - /// The final clock which is exposed to underlying components. + /// The final clock which is exposed to gameplay components. /// public GameplayClock GameplayClock { get; private set; } + /// + /// Whether gameplay is paused. + /// public readonly BindableBool IsPaused = new BindableBool(); /// - /// The decoupled clock used for gameplay. Should be used for seeks and clock control. + /// The adjustable source clock used for gameplay. Should be used for seeks and clock control. /// - protected readonly DecoupleableInterpolatingFramedClock AdjustableClock; + protected readonly DecoupleableInterpolatingFramedClock AdjustableSource; + /// + /// Creates a new . + /// + /// The source used for timing. protected GameplayClockContainer(IClock sourceClock) { RelativeSizeAxes = Axes.Both; - AdjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - AdjustableClock.ChangeSource(sourceClock); + AdjustableSource = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + AdjustableSource.ChangeSource(sourceClock); IsPaused.BindValueChanged(OnIsPausedChanged); } @@ -37,21 +47,24 @@ namespace osu.Game.Screens.Play { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableClock)); + dependencies.CacheAs(GameplayClock = CreateGameplayClock(AdjustableSource)); GameplayClock.IsPaused.BindTo(IsPaused); return dependencies; } + /// + /// Starts gameplay. + /// public virtual void Start() { - if (!AdjustableClock.IsRunning) + if (!AdjustableSource.IsRunning) { // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time // This accounts for the clock source potentially taking time to enter a completely stopped state Seek(GameplayClock.CurrentTime); - AdjustableClock.Start(); + AdjustableSource.Start(); } IsPaused.Value = false; @@ -59,19 +72,22 @@ namespace osu.Game.Screens.Play /// /// Seek to a specific time in gameplay. - /// - /// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track). - /// /// /// The destination time to seek to. - public virtual void Seek(double time) => AdjustableClock.Seek(time); + public virtual void Seek(double time) => AdjustableSource.Seek(time); + /// + /// Stops gameplay. + /// public virtual void Stop() => IsPaused.Value = true; + /// + /// Restarts gameplay. + /// public virtual void Restart() { - AdjustableClock.Seek(0); - AdjustableClock.Stop(); + AdjustableSource.Seek(0); + AdjustableSource.Stop(); if (!IsPaused.Value) Start(); @@ -85,8 +101,26 @@ namespace osu.Game.Screens.Play base.Update(); } - protected abstract void OnIsPausedChanged(ValueChangedEvent isPaused); + /// + /// Invoked when the value of is changed to start or stop the clock. + /// + /// Whether the clock should now be paused. + protected virtual void OnIsPausedChanged(ValueChangedEvent isPaused) + { + if (isPaused.NewValue) + AdjustableSource.Stop(); + else + AdjustableSource.Start(); + } + /// + /// Creates the final which is exposed via DI to be used by gameplay components. + /// + /// + /// Any intermediate clocks such as platform offsets should be applied here. + /// + /// The providing the source time. + /// The final . protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source); } } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 5eb82bf0fa..e7b4645734 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Play /// public const double MINIMUM_SKIP_TIME = 1000; - protected Track Track => (Track)AdjustableClock.Source; + protected Track Track => (Track)AdjustableSource.Source; public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { @@ -84,17 +84,25 @@ namespace osu.Game.Screens.Play Seek(startTime); - AdjustableClock.ProcessFrame(); + AdjustableSource.ProcessFrame(); } protected override void OnIsPausedChanged(ValueChangedEvent isPaused) { + // The source is stopped by a frequency fade first. if (isPaused.NewValue) - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableClock.Stop()); + this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => AdjustableSource.Stop()); else this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); } + /// + /// Seek to a specific time in gameplay. + /// + /// + /// Adjusts for any offsets which have been applied (so the seek may not be the expected point in time on the underlying audio track). + /// + /// The destination time to seek to. public override void Seek(double time) { // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. @@ -146,7 +154,7 @@ namespace osu.Game.Screens.Play public void StopUsingBeatmapClock() { removeSourceClockAdjustments(); - AdjustableClock.ChangeSource(new TrackVirtual(beatmap.Track.Length)); + AdjustableSource.ChangeSource(new TrackVirtual(beatmap.Track.Length)); } private bool speedAdjustmentsApplied; From 314b1646bd22db171ecf35c805860dd578394b41 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 20:47:09 +0900 Subject: [PATCH 13/63] Add xmldoc to MasterGameplayClockContainer --- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index e7b4645734..db0aa23001 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -16,6 +16,16 @@ using osu.Game.Configuration; namespace osu.Game.Screens.Play { + /// + /// A which uses a as a source. + /// + /// This is the most complete which takes into account all user and platform offsets, + /// and provides implementations for user actions such as skipping or adjusting playback rates that may occur during gameplay. + /// + /// + /// + /// This is intended to be used as a single controller for gameplay, or as a reference source for other s. + /// public class MasterGameplayClockContainer : GameplayClockContainer { /// From 44e13a91ade2e0aa18fb8d159eebed51e9b30787 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Apr 2021 20:51:42 +0900 Subject: [PATCH 14/63] Rename test scene to match class --- ...ockContainer.cs => TestSceneMasterGameplayClockContainer.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Gameplay/{TestSceneGameplayClockContainer.cs => TestSceneMasterGameplayClockContainer.cs} (92%) diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs similarity index 92% rename from osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs rename to osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs index 4d5dcabbba..77ada958d7 100644 --- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs @@ -10,7 +10,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Gameplay { [HeadlessTest] - public class TestSceneGameplayClockContainer : OsuTestScene + public class TestSceneMasterGameplayClockContainer : OsuTestScene { [Test] public void TestStartThenElapsedTime() From 5885c24e003bd02c2ad1d1d01756e2dc513b9850 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Apr 2021 16:00:56 +0900 Subject: [PATCH 15/63] Clear current user states on disconnect --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 4bbc420223..03387cfb9f 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -125,6 +125,7 @@ namespace osu.Game.Online.Spectator else { playingUsers.Clear(); + currentUserStates.Clear(); } }, true); } From de9e37857ee380e203668841daecd4c11ae1d839 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Apr 2021 16:06:40 +0900 Subject: [PATCH 16/63] Lock around playingUsers --- .../Spectator/SpectatorStreamingClient.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 03387cfb9f..10a707a4ce 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -47,6 +47,8 @@ namespace osu.Game.Online.Spectator private readonly BindableList playingUsers = new BindableList(); + private readonly Dictionary currentUserStates = new Dictionary(); + [CanBeNull] private IBeatmap currentBeatmap; @@ -60,7 +62,6 @@ namespace osu.Game.Online.Spectator private IBindable> currentMods { get; set; } private readonly SpectatorState currentState = new SpectatorState(); - private readonly Dictionary currentUserStates = new Dictionary(); private bool isPlaying; @@ -124,8 +125,11 @@ namespace osu.Game.Online.Spectator } else { - playingUsers.Clear(); - currentUserStates.Clear(); + lock (userLock) + { + playingUsers.Clear(); + currentUserStates.Clear(); + } } }, true); } @@ -133,11 +137,13 @@ namespace osu.Game.Online.Spectator Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state) { - if (!playingUsers.Contains(userId)) - playingUsers.Add(userId); - lock (userLock) + { + if (!playingUsers.Contains(userId)) + playingUsers.Add(userId); + currentUserStates[userId] = state; + } OnUserBeganPlaying?.Invoke(userId, state); @@ -146,10 +152,11 @@ namespace osu.Game.Online.Spectator Task ISpectatorClient.UserFinishedPlaying(int userId, SpectatorState state) { - playingUsers.Remove(userId); - lock (userLock) + { + playingUsers.Remove(userId); currentUserStates.Remove(userId); + } OnUserFinishedPlaying?.Invoke(userId, state); From 83716ddb089a2e9ac43026aa6440998ccbc0e6ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Apr 2021 16:07:00 +0900 Subject: [PATCH 17/63] Rename currentUserStates -> playingUserStates --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 10a707a4ce..9def009469 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online.Spectator private readonly BindableList playingUsers = new BindableList(); - private readonly Dictionary currentUserStates = new Dictionary(); + private readonly Dictionary playingUserStates = new Dictionary(); [CanBeNull] private IBeatmap currentBeatmap; @@ -128,7 +128,7 @@ namespace osu.Game.Online.Spectator lock (userLock) { playingUsers.Clear(); - currentUserStates.Clear(); + playingUserStates.Clear(); } } }, true); @@ -142,7 +142,7 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); - currentUserStates[userId] = state; + playingUserStates[userId] = state; } OnUserBeganPlaying?.Invoke(userId, state); @@ -155,7 +155,7 @@ namespace osu.Game.Online.Spectator lock (userLock) { playingUsers.Remove(userId); - currentUserStates.Remove(userId); + playingUserStates.Remove(userId); } OnUserFinishedPlaying?.Invoke(userId, state); @@ -298,7 +298,7 @@ namespace osu.Game.Online.Spectator lock (userLock) { - foreach (var (userId, state) in currentUserStates) + foreach (var (userId, state) in playingUserStates) callback(userId, state); } } From c50b526ba09fa2fe98f2d1d43d822a38dc8237f2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Apr 2021 16:48:55 +0900 Subject: [PATCH 18/63] Remove local state dictionary from SpectatorScreen --- .../Online/Spectator/SpectatorStreamingClient.cs | 12 ++++++++++++ osu.Game/Screens/Spectate/SpectatorScreen.cs | 14 +++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 9def009469..13b12d9add 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -284,6 +284,18 @@ namespace osu.Game.Online.Spectator lastSendTime = Time.Current; } + /// + /// Attempts to retrieve the for a currently-playing user. + /// + /// The user. + /// The current for the user, if they're playing. null if the user is not playing. + /// true if successful (the user is playing), false otherwise. + public bool TryGetPlayingUserState(int userId, out SpectatorState state) + { + lock (userLock) + return playingUserStates.TryGetValue(userId, out state); + } + /// /// Bind an action to with the option of running the bound action once immediately. /// diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 7be6c6183b..f554b15abf 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -44,7 +44,6 @@ namespace osu.Game.Screens.Spectate private readonly object stateLock = new object(); private readonly Dictionary userMap = new Dictionary(); - private readonly Dictionary spectatorStates = new Dictionary(); private readonly Dictionary gameplayStates = new Dictionary(); private IBindable> managerUpdated; @@ -107,9 +106,12 @@ namespace osu.Game.Screens.Spectate lock (stateLock) { - foreach (var (userId, state) in spectatorStates) + foreach (var (userId, _) in userMap) { - if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) + if (!spectatorClient.TryGetPlayingUserState(userId, out var userState)) + continue; + + if (beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == userState.BeatmapID)) updateGameplayState(userId); } } @@ -125,7 +127,6 @@ namespace osu.Game.Screens.Spectate if (!userMap.ContainsKey(userId)) return; - spectatorStates[userId] = state; Schedule(() => OnUserStateChanged(userId, state)); updateGameplayState(userId); @@ -138,7 +139,10 @@ namespace osu.Game.Screens.Spectate { Debug.Assert(userMap.ContainsKey(userId)); - var spectatorState = spectatorStates[userId]; + // The user may have stopped playing. + if (!spectatorClient.TryGetPlayingUserState(userId, out var spectatorState)) + return; + var user = userMap[userId]; var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == spectatorState.RulesetID)?.CreateInstance(); From c7183f92f79efe7855fb4b4defacb9224659e3e3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Apr 2021 19:53:55 +0900 Subject: [PATCH 19/63] Rename Restart() -> Reset() --- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 ++-- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 642ede5f1c..ee65384bc9 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -82,9 +82,9 @@ namespace osu.Game.Screens.Play public virtual void Stop() => IsPaused.Value = true; /// - /// Restarts gameplay. + /// Resets this and the source to an initial state ready for gameplay. /// - public virtual void Restart() + public virtual void Reset() { AdjustableSource.Seek(0); AdjustableSource.Stop(); diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index db0aa23001..5ea50cbdc7 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -123,10 +123,10 @@ namespace osu.Game.Screens.Play userOffsetClock.ProcessFrame(); } - public override void Restart() + public override void Reset() { updateRate(); - base.Restart(); + base.Reset(); } /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 841f906b05..27a4fcc291 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -811,7 +811,7 @@ namespace osu.Game.Screens.Play if (GameplayClockContainer.GameplayClock.IsRunning) throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); - GameplayClockContainer.Restart(); + GameplayClockContainer.Reset(); } public override void OnSuspending(IScreen next) From acbf4580a412772b6be6d8e3d60446bbe35b9003 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 19 Apr 2021 19:55:59 +0900 Subject: [PATCH 20/63] Only set initial source in Reset() --- osu.Game/Screens/Play/GameplayClockContainer.cs | 17 +++++++++++++++-- .../Play/MasterGameplayClockContainer.cs | 4 ++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ee65384bc9..3da28f3560 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -29,17 +29,22 @@ namespace osu.Game.Screens.Play /// protected readonly DecoupleableInterpolatingFramedClock AdjustableSource; + /// + /// The source clock. + /// + protected IClock SourceClock { get; private set; } + /// /// Creates a new . /// /// The source used for timing. protected GameplayClockContainer(IClock sourceClock) { + SourceClock = sourceClock; + RelativeSizeAxes = Axes.Both; AdjustableSource = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - AdjustableSource.ChangeSource(sourceClock); - IsPaused.BindValueChanged(OnIsPausedChanged); } @@ -86,6 +91,8 @@ namespace osu.Game.Screens.Play /// public virtual void Reset() { + ChangeSource(SourceClock); + AdjustableSource.Seek(0); AdjustableSource.Stop(); @@ -93,6 +100,12 @@ namespace osu.Game.Screens.Play Start(); } + /// + /// Changes the source clock. + /// + /// The new source. + protected void ChangeSource(IClock sourceClock) => AdjustableSource.ChangeSource(SourceClock = sourceClock); + protected override void Update() { if (!IsPaused.Value) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 5ea50cbdc7..f019e50b60 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play /// public const double MINIMUM_SKIP_TIME = 1000; - protected Track Track => (Track)AdjustableSource.Source; + protected Track Track => (Track)SourceClock; public readonly BindableNumber UserPlaybackRate = new BindableDouble(1) { @@ -164,7 +164,7 @@ namespace osu.Game.Screens.Play public void StopUsingBeatmapClock() { removeSourceClockAdjustments(); - AdjustableSource.ChangeSource(new TrackVirtual(beatmap.Track.Length)); + ChangeSource(new TrackVirtual(beatmap.Track.Length)); } private bool speedAdjustmentsApplied; From a92ae8ce769aea4ff4d2c293ce3e825970985ba3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 13:01:42 +0900 Subject: [PATCH 21/63] Fix Reset() potentially not resetting to the intended start position --- osu.Game/Screens/Play/GameplayClockContainer.cs | 10 +++++++++- .../Screens/Play/MasterGameplayClockContainer.cs | 15 +++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 3da28f3560..e84f34e0d5 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -29,6 +29,11 @@ namespace osu.Game.Screens.Play /// protected readonly DecoupleableInterpolatingFramedClock AdjustableSource; + /// + /// The offset at which to start playing. Affects the time which the clock is reset to via . + /// + protected virtual double StartOffset => 0; + /// /// The source clock. /// @@ -93,9 +98,12 @@ namespace osu.Game.Screens.Play { ChangeSource(SourceClock); - AdjustableSource.Seek(0); + AdjustableSource.Seek(StartOffset); AdjustableSource.Stop(); + // Make sure the gameplay clock takes on the new time, otherwise the adjustable source will be seeked to the gameplay clock time in Start(). + GameplayClock.UnderlyingClock.ProcessFrame(); + if (!IsPaused.Value) Start(); } diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index f019e50b60..05d34fb8d4 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -47,6 +47,9 @@ namespace osu.Game.Screens.Play private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); + protected override double StartOffset => startOffset; + private double startOffset; + private readonly WorkingBeatmap beatmap; private readonly double gameplayStartTime; private readonly bool startAtGameplayStart; @@ -74,27 +77,23 @@ namespace osu.Game.Screens.Play userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); // sane default provided by ruleset. - double startTime = gameplayStartTime; + startOffset = gameplayStartTime; if (!startAtGameplayStart) { - startTime = Math.Min(0, startTime); + startOffset = Math.Min(0, startOffset); // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime; if (firstStoryboardEvent != null) - startTime = Math.Min(startTime, firstStoryboardEvent.Value); + startOffset = Math.Min(startOffset, firstStoryboardEvent.Value); // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // this is not available as an option in the live editor but can still be applied via .osu editing. if (beatmap.BeatmapInfo.AudioLeadIn > 0) - startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); + startOffset = Math.Min(startOffset, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); } - - Seek(startTime); - - AdjustableSource.ProcessFrame(); } protected override void OnIsPausedChanged(ValueChangedEvent isPaused) From 8dd9134e3d9b6304c3a3ad8d4f869ea73394d42e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 13:09:49 +0900 Subject: [PATCH 22/63] Move source clock adjustment application to Start() --- .../Screens/Play/MasterGameplayClockContainer.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 05d34fb8d4..f1d303e245 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -105,6 +105,12 @@ namespace osu.Game.Screens.Play this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); } + public override void Start() + { + addSourceClockAdjustments(); + base.Start(); + } + /// /// Seek to a specific time in gameplay. /// @@ -122,12 +128,6 @@ namespace osu.Game.Screens.Play userOffsetClock.ProcessFrame(); } - public override void Reset() - { - updateRate(); - base.Reset(); - } - /// /// Skip forward to the next valid skip point. /// @@ -164,11 +164,12 @@ namespace osu.Game.Screens.Play { removeSourceClockAdjustments(); ChangeSource(new TrackVirtual(beatmap.Track.Length)); + addSourceClockAdjustments(); } private bool speedAdjustmentsApplied; - private void updateRate() + private void addSourceClockAdjustments() { if (speedAdjustmentsApplied) return; From 88ded95e7587c6345da8ecd186a46aba26600524 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 13:56:13 +0900 Subject: [PATCH 23/63] Ensure clock is set in GCC.Start() --- .../TestSceneMasterGameplayClockContainer.cs | 28 ++++++++++++++++++- .../Screens/Play/GameplayClockContainer.cs | 7 +++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs index 77ada958d7..935bc07733 100644 --- a/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneMasterGameplayClockContainer.cs @@ -25,8 +25,34 @@ namespace osu.Game.Tests.Gameplay Add(gcc = new MasterGameplayClockContainer(working, 0)); }); - AddStep("start track", () => gcc.Start()); + AddStep("start clock", () => gcc.Start()); AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0); } + + [Test] + public void TestElapseThenReset() + { + GameplayClockContainer gcc = null; + + AddStep("create container", () => + { + var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + working.LoadTrack(); + + Add(gcc = new MasterGameplayClockContainer(working, 0)); + }); + + AddStep("start clock", () => gcc.Start()); + AddUntilStep("current time greater 2000", () => gcc.GameplayClock.CurrentTime > 2000); + + double timeAtReset = 0; + AddStep("reset clock", () => + { + timeAtReset = gcc.GameplayClock.CurrentTime; + gcc.Reset(); + }); + + AddAssert("current time < time at reset", () => gcc.GameplayClock.CurrentTime < timeAtReset); + } } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index e84f34e0d5..75b27ed3f3 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.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.Globalization; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Timing; namespace osu.Game.Screens.Play @@ -68,6 +70,9 @@ namespace osu.Game.Screens.Play /// public virtual void Start() { + // Ensure that the source clock is set. + ChangeSource(SourceClock); + if (!AdjustableSource.IsRunning) { // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time @@ -96,8 +101,6 @@ namespace osu.Game.Screens.Play /// public virtual void Reset() { - ChangeSource(SourceClock); - AdjustableSource.Seek(StartOffset); AdjustableSource.Stop(); From 3d6d26039a0a1040964f532b57ac912fef9716c7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 14:09:54 +0900 Subject: [PATCH 24/63] Remove unused usings --- osu.Game/Screens/Play/GameplayClockContainer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 75b27ed3f3..0ada6613d7 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Globalization; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; using osu.Framework.Timing; namespace osu.Game.Screens.Play From 5da18c51a49bb3669dae12f18b9af89668bf873f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 17:27:37 +0900 Subject: [PATCH 25/63] Fix compile error --- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 57b3687d3a..5b8abddd3f 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Gameplay var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gameplayContainer = new GameplayClockContainer(working, 1000, true)); + Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true)); gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) { From 97fb90d9f45c660e4420fbf05477846e010eea35 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 17:35:59 +0900 Subject: [PATCH 26/63] Move clock processing to base.Seek() --- osu.Game/Screens/Play/GameplayClockContainer.cs | 10 +++++++--- osu.Game/Screens/Play/MasterGameplayClockContainer.cs | 3 --- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 0ada6613d7..f6b99c094f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -87,7 +87,13 @@ namespace osu.Game.Screens.Play /// Seek to a specific time in gameplay. /// /// The destination time to seek to. - public virtual void Seek(double time) => AdjustableSource.Seek(time); + public virtual void Seek(double time) + { + AdjustableSource.Seek(time); + + // Manually process to make sure the gameplay clock is correctly updated after a seek. + GameplayClock.UnderlyingClock.ProcessFrame(); + } /// /// Stops gameplay. @@ -102,8 +108,6 @@ namespace osu.Game.Screens.Play AdjustableSource.Seek(StartOffset); AdjustableSource.Stop(); - // Make sure the gameplay clock takes on the new time, otherwise the adjustable source will be seeked to the gameplay clock time in Start(). - GameplayClock.UnderlyingClock.ProcessFrame(); if (!IsPaused.Value) Start(); diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index f1d303e245..1b893a76c5 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -123,9 +123,6 @@ namespace osu.Game.Screens.Play // remove the offset component here because most of the time we want the seek to be aligned to gameplay, not the audio track. // we may want to consider reversing the application of offsets in the future as it may feel more correct. base.Seek(time - totalOffset); - - // manually process frame to ensure GameplayClock is correctly updated after a seek. - userOffsetClock.ProcessFrame(); } /// From a683e5ec34cbbbf1fe08a4eafc1cbaeac7d0969a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 17:36:24 +0900 Subject: [PATCH 27/63] Seek using local method --- osu.Game/Screens/Play/GameplayClockContainer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index f6b99c094f..e9a9c83119 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -105,9 +105,10 @@ namespace osu.Game.Screens.Play /// public virtual void Reset() { - AdjustableSource.Seek(StartOffset); - AdjustableSource.Stop(); + Seek(StartOffset); + // Manually stop the source in order to not affect the IsPaused state. + AdjustableSource.Stop(); if (!IsPaused.Value) Start(); From ec080fcb32fdf2fa1ca7fd4b2d8db1c7a4d2ffc7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 18:25:46 +0900 Subject: [PATCH 28/63] Move seekOffset back to MasterGameplayClockContainer --- osu.Game/Screens/Play/GameplayClockContainer.cs | 7 +------ .../Screens/Play/MasterGameplayClockContainer.cs | 12 +++++++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index e9a9c83119..5cd17d92c4 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -29,11 +29,6 @@ namespace osu.Game.Screens.Play /// protected readonly DecoupleableInterpolatingFramedClock AdjustableSource; - /// - /// The offset at which to start playing. Affects the time which the clock is reset to via . - /// - protected virtual double StartOffset => 0; - /// /// The source clock. /// @@ -105,7 +100,7 @@ namespace osu.Game.Screens.Play /// public virtual void Reset() { - Seek(StartOffset); + Seek(0); // Manually stop the source in order to not affect the IsPaused state. AdjustableSource.Stop(); diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 1b893a76c5..affe24069d 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -47,9 +47,6 @@ namespace osu.Game.Screens.Play private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); - protected override double StartOffset => startOffset; - private double startOffset; - private readonly WorkingBeatmap beatmap; private readonly double gameplayStartTime; private readonly bool startAtGameplayStart; @@ -59,6 +56,7 @@ namespace osu.Game.Screens.Play private FramedOffsetClock platformOffsetClock; private LocalGameplayClock localGameplayClock; private Bindable userAudioOffset; + private double startOffset; public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) : base(beatmap.Track) @@ -94,6 +92,8 @@ namespace osu.Game.Screens.Play if (beatmap.BeatmapInfo.AudioLeadIn > 0) startOffset = Math.Min(startOffset, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); } + + Seek(startOffset); } protected override void OnIsPausedChanged(ValueChangedEvent isPaused) @@ -142,6 +142,12 @@ namespace osu.Game.Screens.Play Seek(skipTarget); } + public override void Reset() + { + base.Reset(); + Seek(startOffset); + } + protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) { // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. From f144661c31a729b6874fad1b720035e3e95d4d23 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 18:26:30 +0900 Subject: [PATCH 29/63] Fix storyboard sample test scene --- .../Gameplay/TestSceneStoryboardSamples.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 5b8abddd3f..bbab9ae94d 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -20,6 +20,7 @@ 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; using osu.Game.Storyboards; @@ -67,15 +68,17 @@ namespace osu.Game.Tests.Gameplay var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gameplayContainer = new MasterGameplayClockContainer(working, 0)); - - gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) + Add(gameplayContainer = new MasterGameplayClockContainer(working, 0) { - Clock = gameplayContainer.GameplayClock + IsPaused = { Value = true }, + Child = new FrameStabilityContainer + { + Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) + } }); }); - AddStep("start time", () => gameplayContainer.Start()); + AddStep("reset clock", () => gameplayContainer.Start()); AddUntilStep("sample played", () => sample.RequestedPlaying); AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue); @@ -92,11 +95,13 @@ namespace osu.Game.Tests.Gameplay var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true)); - - gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) + Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true) { - Clock = gameplayContainer.GameplayClock + IsPaused = { Value = true }, + Child = new FrameStabilityContainer + { + Child = sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) + } }); }); From c5d6b6ea8d05578cfac4ae8697f15e37d8d84da3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 18:41:09 +0900 Subject: [PATCH 30/63] Fix tests failing intermittently This was due to this code happening in UpdateAfterChildren(), after the GCC has processed one frame. During this time, the clock could have advanced an arbitrary amount. The cause of this is the removal of the Task.Run() to set the clock in Restart() (now called Reset()) which changed the timing, so it only worked before due to pure luck. --- osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index dccde366c2..f5f17a0bc1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -130,9 +130,9 @@ namespace osu.Game.Tests.Visual.Gameplay public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime; - protected override void UpdateAfterChildren() + protected override void Update() { - base.UpdateAfterChildren(); + base.Update(); if (!FirstFrameClockTime.HasValue) { From a9e4a0ed50dcfbd81487afbc14ba10fd8cd7463f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 21:17:24 +0900 Subject: [PATCH 31/63] Fix potentially starting play when finished The UserFinishedPlaying event may trigger before the event is subscribed to by SpectatorScreen. For such cases, an extra check is done to make sure the user is _actually_ playing. --- osu.Game/Screens/Spectate/SpectatorScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index f554b15abf..ed01d56801 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -127,6 +127,10 @@ namespace osu.Game.Screens.Spectate if (!userMap.ContainsKey(userId)) return; + // The user may have stopped playing. + if (!spectatorClient.TryGetPlayingUserState(userId, out _)) + return; + Schedule(() => OnUserStateChanged(userId, state)); updateGameplayState(userId); From 4cc3321d54b2e2e57ebb09b1cb240135d421ec4d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 20 Apr 2021 21:20:08 +0900 Subject: [PATCH 32/63] Fix potential doubling of events --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 13b12d9add..378096c7fb 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -303,13 +303,14 @@ namespace osu.Game.Online.Spectator /// Whether the action provided in should be run once immediately for all users currently playing. public void BindUserBeganPlaying(Action callback, bool runOnceImmediately = false) { - OnUserBeganPlaying += callback; - - if (!runOnceImmediately) - return; - + // The lock is taken before the event is subscribed to to prevent doubling of events. lock (userLock) { + OnUserBeganPlaying += callback; + + if (!runOnceImmediately) + return; + foreach (var (userId, state) in playingUserStates) callback(userId, state); } From e90d791754776b5b5c51d42083145a665b9912c6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 21 Apr 2021 09:14:19 +0300 Subject: [PATCH 33/63] Add base "classic" mod --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 17 +-------------- osu.Game/Rulesets/Mods/ModClassic.cs | 24 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ModClassic.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 882f848190..77dea5b0dc 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; -using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -16,22 +15,8 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset + public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset { - public override string Name => "Classic"; - - public override string Acronym => "CL"; - - public override double ScoreMultiplier => 1; - - public override IconUsage? Icon => FontAwesome.Solid.History; - - public override string Description => "Feeling nostalgic?"; - - public override bool Ranked => false; - - public override ModType Type => ModType.Conversion; - [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true); diff --git a/osu.Game/Rulesets/Mods/ModClassic.cs b/osu.Game/Rulesets/Mods/ModClassic.cs new file mode 100644 index 0000000000..f1207ec188 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModClassic.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Sprites; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModClassic : Mod + { + public override string Name => "Classic"; + + public override string Acronym => "CL"; + + public override double ScoreMultiplier => 1; + + public override IconUsage? Icon => FontAwesome.Solid.History; + + public override string Description => "Feeling nostalgic?"; + + public override bool Ranked => false; + + public override ModType Type => ModType.Conversion; + } +} From e3398d8f1fdfbbc57b6a3ae38e4ec9648f8505f6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 21 Apr 2021 09:14:31 +0300 Subject: [PATCH 34/63] Implement "classic" mod for all other legacy rulesets Currently empty, automatically handled in game to not be selectable (see `Mod.HasImplementation`) --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 1 + osu.Game.Rulesets.Catch/Mods/CatchModClassic.cs | 11 +++++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 1 + osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs | 11 +++++++++++ osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs | 11 +++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 1 + 6 files changed, 36 insertions(+) create mode 100644 osu.Game.Rulesets.Catch/Mods/CatchModClassic.cs create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs create mode 100644 osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index f4ddbd3021..ab877c21c1 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -114,6 +114,7 @@ namespace osu.Game.Rulesets.Catch return new Mod[] { new CatchModDifficultyAdjust(), + new CatchModClassic(), }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModClassic.cs b/osu.Game.Rulesets.Catch/Mods/CatchModClassic.cs new file mode 100644 index 0000000000..9624e84018 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Mods/CatchModClassic.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Catch.Mods +{ + public class CatchModClassic : ModClassic + { + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 88b63606b9..b3889bc7d3 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -239,6 +239,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModDualStages(), new ManiaModMirror(), new ManiaModDifficultyAdjust(), + new ManiaModClassic(), new ManiaModInvert(), new ManiaModConstantSpeed() }; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs new file mode 100644 index 0000000000..073dda9de8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModClassic.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModClassic : ModClassic + { + } +} diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs new file mode 100644 index 0000000000..5a4d18be98 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModClassic.cs @@ -0,0 +1,11 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Taiko.Mods +{ + public class TaikoModClassic : ModClassic + { + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 56f58f404b..f4e158ec32 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -135,6 +135,7 @@ namespace osu.Game.Rulesets.Taiko { new TaikoModRandom(), new TaikoModDifficultyAdjust(), + new TaikoModClassic(), }; case ModType.Automation: From 1a715b2926de6c3a9e3322474c4fe1f20ec11a5e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 21 Apr 2021 09:16:28 +0300 Subject: [PATCH 35/63] Append "classic" mod to legacy scores --- osu.Game/Scoring/ScoreInfo.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 222f69b025..bf131658ea 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -65,14 +65,19 @@ namespace osu.Game.Scoring { get { - if (mods != null) - return mods; - - if (localAPIMods == null) - return Array.Empty(); + Mod[] scoreMods = Array.Empty(); var rulesetInstance = Ruleset.CreateInstance(); - return apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + if (mods != null) + scoreMods = mods; + else if (localAPIMods != null) + scoreMods = apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); + + if (IsLegacyScore) + scoreMods = scoreMods.Append(rulesetInstance.GetAllMods().OfType().Single()).ToArray(); + + return scoreMods; } set { From fb848f7544435470f330aec600b2fc14e5313936 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 21 Apr 2021 16:33:14 +0900 Subject: [PATCH 36/63] Rename to MasterGameplayClock --- .../Screens/Play/MasterGameplayClockContainer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index affe24069d..fcbc6fae15 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play private FramedOffsetClock userOffsetClock; private FramedOffsetClock platformOffsetClock; - private LocalGameplayClock localGameplayClock; + private MasterGameplayClock masterGameplayClock; private Bindable userAudioOffset; private double startOffset; @@ -157,7 +157,7 @@ namespace osu.Game.Screens.Play // the final usable gameplay clock with user-set offsets applied. userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); - return localGameplayClock = new LocalGameplayClock(userOffsetClock); + return masterGameplayClock = new MasterGameplayClock(userOffsetClock); } /// @@ -180,8 +180,8 @@ namespace osu.Game.Screens.Play Track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); Track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); - localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); + masterGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); + masterGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); speedAdjustmentsApplied = true; } @@ -194,8 +194,8 @@ namespace osu.Game.Screens.Play Track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); Track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); - localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); + masterGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); + masterGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); speedAdjustmentsApplied = false; } @@ -218,13 +218,13 @@ namespace osu.Game.Screens.Play } } - private class LocalGameplayClock : GameplayClock + private class MasterGameplayClock : GameplayClock { public readonly List> MutableNonGameplayAdjustments = new List>(); public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; - public LocalGameplayClock(FramedOffsetClock underlyingClock) + public MasterGameplayClock(FramedOffsetClock underlyingClock) : base(underlyingClock) { } From c6c91cd9a5ce76554b277b0e7520e0aeb4312cef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Apr 2021 18:05:26 +0900 Subject: [PATCH 37/63] Refactor `WaveformOpacityMenuItem` to not receive whole config --- osu.Game/Screens/Edit/Editor.cs | 2 +- .../{WaveformOpacityMenu.cs => WaveformOpacityMenuItem.cs} | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) rename osu.Game/Screens/Edit/{WaveformOpacityMenu.cs => WaveformOpacityMenuItem.cs} (85%) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index fffea65456..360fbb36db 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Edit { Items = new[] { - new WaveformOpacityMenu(config) + new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), } } } diff --git a/osu.Game/Screens/Edit/WaveformOpacityMenu.cs b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs similarity index 85% rename from osu.Game/Screens/Edit/WaveformOpacityMenu.cs rename to osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs index 5d209ae141..053c2fa4b0 100644 --- a/osu.Game/Screens/Edit/WaveformOpacityMenu.cs +++ b/osu.Game/Screens/Edit/WaveformOpacityMenuItem.cs @@ -4,18 +4,17 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; -using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Edit { - internal class WaveformOpacityMenu : MenuItem + internal class WaveformOpacityMenuItem : MenuItem { private readonly Bindable waveformOpacity; private readonly Dictionary menuItemLookup = new Dictionary(); - public WaveformOpacityMenu(OsuConfigManager config) + public WaveformOpacityMenuItem(Bindable waveformOpacity) : base("Waveform opacity") { Items = new[] @@ -26,7 +25,7 @@ namespace osu.Game.Screens.Edit createMenuItem(1f), }; - waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); + this.waveformOpacity = waveformOpacity; waveformOpacity.BindValueChanged(opacity => { foreach (var kvp in menuItemLookup) From 9d8f0c854d1bce0763c1e7e4113321a8e0646944 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Apr 2021 18:05:40 +0900 Subject: [PATCH 38/63] Setup configuration item for editor hit animations --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Screens/Edit/Editor.cs | 3 ++- .../Screens/Edit/HitAnimationsMenuItem.cs | 21 +++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Edit/HitAnimationsMenuItem.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index f9b1c9618b..09412b1f1b 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -143,6 +143,7 @@ namespace osu.Game.Configuration SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full); SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f); + SetDefault(OsuSetting.EditorHitAnimations, false); } public OsuConfigManager(Storage storage) @@ -266,6 +267,7 @@ namespace osu.Game.Configuration GameplayDisableWinKey, SeasonalBackgroundMode, EditorWaveformOpacity, + EditorHitAnimations, DiscordRichPresence, AutomaticallyDownloadWhenSpectating, ShowOnlineExplicitContent, diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 360fbb36db..da0e9ebbaf 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -224,9 +224,10 @@ namespace osu.Game.Screens.Edit }, new MenuItem("View") { - Items = new[] + Items = new MenuItem[] { new WaveformOpacityMenuItem(config.GetBindable(OsuSetting.EditorWaveformOpacity)), + new HitAnimationsMenuItem(config.GetBindable(OsuSetting.EditorHitAnimations)) } } } diff --git a/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs b/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs new file mode 100644 index 0000000000..fb7ab39f7a --- /dev/null +++ b/osu.Game/Screens/Edit/HitAnimationsMenuItem.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Bindables; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit +{ + internal class HitAnimationsMenuItem : ToggleMenuItem + { + [UsedImplicitly] + private readonly Bindable hitAnimations; + + public HitAnimationsMenuItem(Bindable hitAnimations) + : base("Hit animations") + { + State.BindTo(this.hitAnimations = hitAnimations); + } + } +} From 47a4a07024b6557d475b92f1356ee15c0828376c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Apr 2021 19:15:10 +0900 Subject: [PATCH 39/63] Split out animation triggering of `MainCirclePiece` to be interface driven --- .../Objects/Drawables/DrawableHitCircle.cs | 2 ++ .../Skinning/Default/IMainCirclePiece.cs | 17 +++++++++++++++++ .../Skinning/Default/MainCirclePiece.cs | 7 ++----- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 8 +++----- 4 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/Default/IMainCirclePiece.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index df77ec2693..fb6c110b3c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -182,6 +182,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // todo: temporary / arbitrary, used for lifetime optimisation. this.Delay(800).FadeOut(); + (CirclePiece.Drawable as IMainCirclePiece)?.Animate(state); + switch (state) { case ArmedState.Idle: diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/IMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/IMainCirclePiece.cs new file mode 100644 index 0000000000..17a1e29094 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/Default/IMainCirclePiece.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Skinning.Default +{ + public interface IMainCirclePiece + { + /// + /// Begins animating this . + /// + /// The of the related . + void Animate(ArmedState state); + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs index 46aeadc59b..b46baa00ba 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs @@ -13,7 +13,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning.Default { - public class MainCirclePiece : CompositeDrawable + public class MainCirclePiece : CompositeDrawable, IMainCirclePiece { private readonly CirclePiece circle; private readonly RingPiece ring; @@ -67,12 +67,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default }, true); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); - - drawableObject.ApplyCustomUpdateState += updateState; - updateState(drawableObject, drawableObject.State.Value); } - private void updateState(DrawableHitObject drawableObject, ArmedState state) + public void Animate(ArmedState state) { using (BeginAbsoluteSequence(drawableObject.StateUpdateTime)) glow.FadeOut(400); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 545e80a709..cf62165929 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -19,7 +20,7 @@ using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Osu.Skinning.Legacy { - public class LegacyMainCirclePiece : CompositeDrawable + public class LegacyMainCirclePiece : CompositeDrawable, IMainCirclePiece { private readonly string priorityLookup; private readonly bool hasNumber; @@ -138,12 +139,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); if (hasNumber) indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); - - drawableObject.ApplyCustomUpdateState += updateState; - updateState(drawableObject, drawableObject.State.Value); } - private void updateState(DrawableHitObject drawableObject, ArmedState state) + public void Animate(ArmedState state) { const double legacy_fade_duration = 240; From f2824a222a13f924df12f4479ccf3b8d68e5f6b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Apr 2021 19:41:15 +0900 Subject: [PATCH 40/63] Adjust existing fades to close match stable editor --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 5fdb79cbbd..dfb71d1d22 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit case DrawableHitCircle circle: // also handles slider heads circle.ApproachCircle - .FadeOutFromOne(editor_hit_object_fade_out_extension) + .FadeOutFromOne(editor_hit_object_fade_out_extension * 4) .Expire(); break; } @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Edit hitObject.RemoveTransform(existing); - using (hitObject.BeginAbsoluteSequence(existing.StartTime)) + using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime)) hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); } } From 0f70469d1c33f392def6ae1070f51cdad941643c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Apr 2021 19:44:17 +0900 Subject: [PATCH 41/63] Only apply custom editor overrides if hit animations is disabled --- .../Edit/DrawableOsuEditRuleset.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index dfb71d1d22..b8d0637e90 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -3,8 +3,11 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -27,8 +30,16 @@ namespace osu.Game.Rulesets.Osu.Edit private class OsuEditPlayfield : OsuPlayfield { + private Bindable hitAnimations; + protected override GameplayCursorContainer CreateCursor() => null; + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + hitAnimations = config.GetBindable(OsuSetting.EditorHitAnimations); + } + protected override void OnNewDrawableHitObject(DrawableHitObject d) { d.ApplyCustomUpdateState += updateState; @@ -42,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState(DrawableHitObject hitObject, ArmedState state) { - if (state == ArmedState.Idle) + if (state == ArmedState.Idle || hitAnimations.Value) return; // adjust the visuals of certain object types to make them stay on screen for longer than usual. @@ -60,6 +71,15 @@ namespace osu.Game.Rulesets.Osu.Edit circle.ApproachCircle .FadeOutFromOne(editor_hit_object_fade_out_extension * 4) .Expire(); + + circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint); + + var circlePieceDrawable = circle.CirclePiece.Drawable; + + // clear any explode animation logic. + circlePieceDrawable.ApplyTransformsAt(circle.HitStateUpdateTime, true); + circlePieceDrawable.ClearTransformsAfter(circle.HitStateUpdateTime, true); + break; } From 60b702549dd2bfc42f21a73831396edf9c7dedac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 14:20:49 +0900 Subject: [PATCH 42/63] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 3324af7c51..7b97103851 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fcd1ed6987..b27e00f59d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 34810a3106..d07e16ea56 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 21f34be19fe58a3cfaca1f2182dc1054aed0aca4 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 22 Apr 2021 14:42:10 +0900 Subject: [PATCH 43/63] Add support for per-ruleset sample playback when switching rulesets (via toolbar) --- .../Overlays/Toolbar/ToolbarRulesetSelector.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 905d5b44c6..eb235632e8 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.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.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -13,6 +14,8 @@ using osu.Framework.Input.Events; using osuTK.Input; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; namespace osu.Game.Overlays.Toolbar { @@ -20,6 +23,8 @@ namespace osu.Game.Overlays.Toolbar { protected Drawable ModeButtonLine { get; private set; } + private readonly Dictionary selectionSamples = new Dictionary(); + public ToolbarRulesetSelector() { RelativeSizeAxes = Axes.Y; @@ -27,7 +32,7 @@ namespace osu.Game.Overlays.Toolbar } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { AddRangeInternal(new[] { @@ -54,6 +59,9 @@ namespace osu.Game.Overlays.Toolbar } } }); + + foreach (var ruleset in Rulesets.AvailableRulesets) + selectionSamples[ruleset.ShortName] = audio.Samples.Get($"UI/ruleset-select-{ruleset.ShortName}"); } protected override void LoadComplete() @@ -72,6 +80,10 @@ namespace osu.Game.Overlays.Toolbar if (SelectedTab != null) { ModeButtonLine.MoveToX(SelectedTab.DrawPosition.X, !hasInitialPosition ? 0 : 200, Easing.OutQuint); + + if (hasInitialPosition) + selectionSamples[SelectedTab.Value.ShortName]?.Play(); + hasInitialPosition = true; } }); From ea3bb07924f6be8126dc8bc1894478db9f00ba82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 14:51:14 +0900 Subject: [PATCH 44/63] Add test that fails on incorrect system/info message ordering --- .../Online/TestSceneStandAloneChatDisplay.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index 01e67b1681..165fff99dd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -83,6 +83,28 @@ namespace osu.Game.Tests.Visual.Online }; }); + [Test] + public void TestSystemMessageOrdering() + { + var standardMessage = new Message(messageIdSequence++) + { + Sender = admin, + Content = "I am a wang!" + }; + + var infoMessage1 = new InfoMessage($"the system is calling {messageIdSequence++}"); + var infoMessage2 = new InfoMessage($"the system is calling {messageIdSequence++}"); + + AddStep("message from admin", () => testChannel.AddNewMessages(standardMessage)); + AddStep("message from system", () => testChannel.AddNewMessages(infoMessage1)); + AddStep("message from system", () => testChannel.AddNewMessages(infoMessage2)); + + AddAssert("message order is correct", () => testChannel.Messages.Count == 3 + && testChannel.Messages[0] == standardMessage + && testChannel.Messages[1] == infoMessage1 + && testChannel.Messages[2] == infoMessage2); + } + [Test] public void TestManyMessages() { From 3befb49ea9e5ed6420c053ba4b07bd58f56fac75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 14:51:57 +0900 Subject: [PATCH 45/63] Fix system messages always being displayed above standard messages Closes https://github.com/ppy/osu/issues/12509. --- osu.Game/Online/Chat/InfoMessage.cs | 4 +--- osu.Game/Online/Chat/Message.cs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Chat/InfoMessage.cs b/osu.Game/Online/Chat/InfoMessage.cs index 8dce188804..cea336aae2 100644 --- a/osu.Game/Online/Chat/InfoMessage.cs +++ b/osu.Game/Online/Chat/InfoMessage.cs @@ -8,10 +8,8 @@ namespace osu.Game.Online.Chat { public class InfoMessage : LocalMessage { - private static int infoID = -1; - public InfoMessage(string message) - : base(infoID--) + : base(null) { Timestamp = DateTimeOffset.Now; Content = message; diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index 2e41038a59..30753b3920 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -59,7 +59,7 @@ namespace osu.Game.Online.Chat return Id.Value.CompareTo(other.Id.Value); } - public virtual bool Equals(Message other) => Id == other?.Id; + public virtual bool Equals(Message other) => Id.HasValue && Id == other?.Id; // ReSharper disable once ImpureMethodCallOnReadonlyValueField public override int GetHashCode() => Id.GetHashCode(); From b37c5a8749422ccdd93caeeea815aa5013f26353 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 14:59:57 +0900 Subject: [PATCH 46/63] Rollback hold note placement when length is zero --- .../Edit/Blueprints/HoldNotePlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index a13afdfffe..093a8da24f 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return; base.OnMouseUp(e); - EndPlacement(true); + EndPlacement(HitObject.Duration > 0); } private double originalStartTime; From cf1e3ea98873897de95984f79846ca7282a7581f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 15:41:21 +0900 Subject: [PATCH 47/63] Add failing test covering quick shift-rightclick deletion in placement mode --- .../Editing/TestSceneEditorSelection.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs index 99f31b0c2a..c9b6d2e376 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Tests.Beatmaps; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -156,9 +157,35 @@ namespace osu.Game.Tests.Visual.Editing } [Test] - public void TestQuickDeleteRemovesObject() + public void TestQuickDeleteRemovesObjectInPlacement() { - var addedObject = new HitCircle { StartTime = 1000 }; + var addedObject = new HitCircle + { + StartTime = 0, + Position = OsuPlayfield.BASE_SIZE * 0.5f + }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("enter placement mode", () => InputManager.PressKey(Key.Number2)); + + moveMouseToObject(() => addedObject); + + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); + } + + [Test] + public void TestQuickDeleteRemovesObjectInSelection() + { + var addedObject = new HitCircle + { + StartTime = 0, + Position = OsuPlayfield.BASE_SIZE * 0.5f + }; AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); From 9a7bf8109ff7029ad76e2d60b389d660d062ec57 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 15:27:08 +0900 Subject: [PATCH 48/63] Allow certain mouse input to pass through `PlacementBlueprints` to the selection logic --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 6c1cd01796..4ad8c815fe 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Edit { @@ -128,8 +129,11 @@ namespace osu.Game.Rulesets.Edit case DoubleClickEvent _: return false; - case MouseButtonEvent _: - return true; + case MouseButtonEvent mouse: + // placement blueprints should generally block mouse from reaching underlying components (ie. performing clicks on interface buttons). + // for now, the one exception we want to allow is when using a non-main mouse button when shift is pressed, which is used to trigger object deletion + // while in placement mode. + return mouse.Button == MouseButton.Left || !mouse.ShiftPressed; default: return false; From 3e1002fbf36650b9a91a6a04df9d15c0d220f8f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 17:06:08 +0900 Subject: [PATCH 49/63] Improve osu!catch caught fruit placement algorithm --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 29 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index d045dcf16a..8edc5d0eb2 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -53,6 +53,11 @@ namespace osu.Game.Rulesets.Catch.UI /// public const double BASE_SPEED = 1.0; + /// + /// The amount by which caught fruit should be scaled down to fit on the plate. + /// + private const float caught_fruit_scale_adjust = 0.5f; + [NotNull] private readonly Container trailsTarget; @@ -240,7 +245,7 @@ namespace osu.Game.Rulesets.Catch.UI if (result.IsHit) { - var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2); + var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X); if (CatchFruitOnPlate) placeCaughtObject(palpableObject, positionInStack); @@ -470,7 +475,7 @@ namespace osu.Game.Rulesets.Catch.UI caughtObject.CopyStateFrom(drawableObject); caughtObject.Anchor = Anchor.TopCentre; caughtObject.Position = position; - caughtObject.Scale /= 2; + caughtObject.Scale *= caught_fruit_scale_adjust; caughtObjectContainer.Add(caughtObject); @@ -480,19 +485,21 @@ namespace osu.Game.Rulesets.Catch.UI private Vector2 computePositionInStack(Vector2 position, float displayRadius) { - const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2; - const float allowance = 10; + // this is taken from osu-stable (lenience should be 10 * 10 at standard scale). + const float lenience_adjust = 10 / CatchHitObject.OBJECT_RADIUS; - while (caughtObjectContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2))) + float adjustedRadius = displayRadius * lenience_adjust; + float checkDistance = MathF.Pow(adjustedRadius, 2); + + // offset fruit vertically to better place "above" the plate. + position.Y -= 5; + + while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance)) { - float diff = (displayRadius + radius_div_2) / allowance; - - position.X += (RNG.NextSingle() - 0.5f) * diff * 2; - position.Y -= RNG.NextSingle() * diff; + position.X += RNG.NextSingle(-adjustedRadius, adjustedRadius); + position.Y -= RNG.NextSingle(0, 5); } - position.X = Math.Clamp(position.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); - return position; } From 84a713822306f5a6dd4897b58e3894f6b669190f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 16:56:23 +0900 Subject: [PATCH 50/63] Update tests to better support stack regeneration cases --- .../TestSceneCatcher.cs | 33 ++++++++++++++----- osu.Game.Rulesets.Catch/UI/Catcher.cs | 7 +++- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 48efd73222..f8e278a486 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.UI; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; @@ -170,16 +171,25 @@ namespace osu.Game.Rulesets.Catch.Tests } [Test] - public void TestCatcherStacking() + public void TestCatcherRandomStacking() + { + AddStep("catch more fruits", () => attemptCatch(() => new Fruit + { + X = (RNG.NextSingle() - 0.5f) * CatcherArea.CATCHER_SIZE + }, 50)); + } + + [Test] + public void TestCatcherStackingSameCaughtPosition() { AddStep("catch fruit", () => attemptCatch(new Fruit())); checkPlate(1); - AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9)); + AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9)); checkPlate(10); AddAssert("caught objects are stacked", () => - catcher.CaughtObjects.All(obj => obj.Y <= 0) && - catcher.CaughtObjects.Any(obj => obj.Y == 0) && - catcher.CaughtObjects.Any(obj => obj.Y < -20)); + catcher.CaughtObjects.All(obj => obj.Y <= Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) && + catcher.CaughtObjects.Any(obj => obj.Y == Catcher.CAUGHT_FRUIT_VERTICAL_OFFSET) && + catcher.CaughtObjects.Any(obj => obj.Y < -25)); } [Test] @@ -189,11 +199,11 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet())); AddAssert("tiny droplet is exploded", () => catcher.CaughtObjects.Count() == 1 && droppedObjectContainer.Count == 1); AddUntilStep("wait explosion", () => !droppedObjectContainer.Any()); - AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9)); + AddStep("catch more fruits", () => attemptCatch(() => new Fruit(), 9)); AddStep("explode", () => catcher.Explode()); AddAssert("fruits are exploded", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10); AddUntilStep("wait explosion", () => !droppedObjectContainer.Any()); - AddStep("catch fruits", () => attemptCatch(new Fruit(), 10)); + AddStep("catch fruits", () => attemptCatch(() => new Fruit(), 10)); AddStep("drop", () => catcher.Drop()); AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10); } @@ -222,10 +232,15 @@ namespace osu.Game.Rulesets.Catch.Tests private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state); - private void attemptCatch(CatchHitObject hitObject, int count = 1) + private void attemptCatch(CatchHitObject hitObject) + { + attemptCatch(() => hitObject, 1); + } + + private void attemptCatch(Func hitObject, int count) { for (var i = 0; i < count; i++) - attemptCatch(hitObject, out _, out _); + attemptCatch(hitObject(), out _, out _); } private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 8edc5d0eb2..4fc9e32a73 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -53,6 +53,11 @@ namespace osu.Game.Rulesets.Catch.UI /// public const double BASE_SPEED = 1.0; + /// + /// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught". + /// + public const float CAUGHT_FRUIT_VERTICAL_OFFSET = -5; + /// /// The amount by which caught fruit should be scaled down to fit on the plate. /// @@ -492,7 +497,7 @@ namespace osu.Game.Rulesets.Catch.UI float checkDistance = MathF.Pow(adjustedRadius, 2); // offset fruit vertically to better place "above" the plate. - position.Y -= 5; + position.Y += CAUGHT_FRUIT_VERTICAL_OFFSET; while (caughtObjectContainer.Any(f => Vector2Extensions.DistanceSquared(f.Position, position) < checkDistance)) { From dc2bc462b8c68b44b6e927ec84bbf87fc0586df3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 17:27:23 +0900 Subject: [PATCH 51/63] Expose internal catcher width calculation methods --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 4fc9e32a73..0d6a577d1e 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -212,13 +212,13 @@ namespace osu.Game.Rulesets.Catch.UI /// Calculates the width of the area used for attempting catches in gameplay. /// /// The scale of the catcher. - internal static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE; + public static float CalculateCatchWidth(Vector2 scale) => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE; /// /// Calculates the width of the area used for attempting catches in gameplay. /// /// The beatmap difficulty. - internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); + public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); /// /// Determine if this catcher can catch a in the current position. From 2203552e9e23528e79c63024d80186e4f5034bc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 17:32:24 +0900 Subject: [PATCH 52/63] Add stacking test logic to `TestSceneCatcherArea` for skinned testing --- .../TestSceneCatcher.cs | 3 +- .../TestSceneCatcherArea.cs | 36 +++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index f8e278a486..517027a9fc 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Catch.Tests { @@ -175,7 +176,7 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("catch more fruits", () => attemptCatch(() => new Fruit { - X = (RNG.NextSingle() - 0.5f) * CatcherArea.CATCHER_SIZE + X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(Vector2.One) }, 50)); } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 1cbfa6338e..3e4ee5ffa9 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -8,6 +8,8 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; @@ -31,12 +33,32 @@ namespace osu.Game.Rulesets.Catch.Tests private float circleSize; + private ScheduledDelegate addManyFruit; + + private BeatmapDifficulty beatmapDifficulty; + public TestSceneCatcherArea() { AddSliderStep("circle size", 0, 8, 5, createCatcher); AddToggleStep("hyper dash", t => this.ChildrenOfType().ForEach(area => area.ToggleHyperDash(t))); - AddStep("catch fruit", () => attemptCatch(new Fruit())); + AddStep("catch centered fruit", () => attemptCatch(new Fruit())); + AddStep("catch many random fruit", () => + { + int count = 50; + + addManyFruit?.Cancel(); + addManyFruit = Scheduler.AddDelayed(() => + { + attemptCatch(new Fruit + { + X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty), + }); + + if (count-- == 0) + addManyFruit?.Cancel(); + }, 50, true); + }); AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true })); AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit())); AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true })); @@ -45,10 +67,7 @@ namespace osu.Game.Rulesets.Catch.Tests private void attemptCatch(Fruit fruit) { fruit.X = fruit.OriginalX + catcher.X; - fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty - { - CircleSize = circleSize - }); + fruit.ApplyDefaults(new ControlPointInfo(), beatmapDifficulty); foreach (var area in this.ChildrenOfType()) { @@ -71,6 +90,11 @@ namespace osu.Game.Rulesets.Catch.Tests { circleSize = size; + beatmapDifficulty = new BeatmapDifficulty + { + CircleSize = circleSize + }; + SetContents(() => { var droppedObjectContainer = new Container @@ -84,7 +108,7 @@ namespace osu.Game.Rulesets.Catch.Tests Children = new Drawable[] { droppedObjectContainer, - new TestCatcherArea(droppedObjectContainer, new BeatmapDifficulty { CircleSize = size }) + new TestCatcherArea(droppedObjectContainer, beatmapDifficulty) { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, From bdf07ad59a7cfc0abb796133001da52bd83f314b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 17:47:03 +0900 Subject: [PATCH 53/63] Limit catching towards the centre of the plate (to emulate actual gameplay) --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 3e4ee5ffa9..ad404e1f63 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Tests { attemptCatch(new Fruit { - X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty), + X = (RNG.NextSingle() - 0.5f) * Catcher.CalculateCatchWidth(beatmapDifficulty) * 0.6f, }); if (count-- == 0) From 1884c18a2cc3d169012f89da64a16d65c7cd267f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 18:12:03 +0900 Subject: [PATCH 54/63] Ignore movement operations which have no offset --- osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index b1afbe0d61..f70e063ba9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -519,7 +519,8 @@ namespace osu.Game.Screens.Edit.Compose.Components // Apply the start time at the newly snapped-to position double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime; - Beatmap.PerformOnSelection(obj => obj.StartTime += offset); + if (offset != 0) + Beatmap.PerformOnSelection(obj => obj.StartTime += offset); } return true; From 8a6267580a207fc63c88ed3a962037eeff7ad162 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 18:44:14 +0900 Subject: [PATCH 55/63] Fix nullref --- osu.Game/Scoring/ScoreInfo.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index bf131658ea..a6faaf6379 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -65,9 +65,11 @@ namespace osu.Game.Scoring { get { - Mod[] scoreMods = Array.Empty(); + var rulesetInstance = Ruleset?.CreateInstance(); + if (rulesetInstance == null) + return mods ?? Array.Empty(); - var rulesetInstance = Ruleset.CreateInstance(); + Mod[] scoreMods = Array.Empty(); if (mods != null) scoreMods = mods; From a5364b224f083be5d55a31c95203f7a65069f3cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 18:47:04 +0900 Subject: [PATCH 56/63] Add simple key based time nudging support to editor --- .../Editing/TestSceneEditorSelection.cs | 22 ++++++++++ .../Input/Bindings/GlobalActionContainer.cs | 8 ++++ .../Timeline/TimelineBlueprintContainer.cs | 42 ++++++++++++++++++- 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs index 99f31b0c2a..c783ea1448 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs @@ -41,6 +41,28 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestNudgeSelection() + { + HitCircle[] addedObjects = null; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(50) }, + new HitCircle { StartTime = 300, Position = new Vector2(100) }, + new HitCircle { StartTime = 400, Position = new Vector2(150) }, + })); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("nudge forwards", () => InputManager.Key(Key.K)); + AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 100); + + AddStep("nudge backwards", () => InputManager.Key(Key.J)); + AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100); + } + [Test] public void TestBasicSelect() { diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index e414e12dd1..6717de5658 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -71,6 +71,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode), + new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft), + new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight), }; public IEnumerable InGameKeyBindings => new[] @@ -251,5 +253,11 @@ namespace osu.Game.Input.Bindings [Description("Verify mode")] EditorVerifyMode, + + [Description("Nudge selection left")] + EditorNudgeLeft, + + [Description("Nudge selection right")] + EditorNudgeRight } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 7a3781a981..3555bc2800 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -12,9 +12,11 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Graphics; +using osu.Game.Input.Bindings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -237,10 +239,48 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - internal class TimelineSelectionHandler : SelectionHandler + internal class TimelineSelectionHandler : SelectionHandler, IKeyBindingHandler { // for now we always allow movement. snapping is provided by the Timeline's "distance" snap implementation public override bool HandleMovement(MoveSelectionEvent moveEvent) => true; + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.EditorNudgeLeft: + nudgeSelection(-1); + return true; + + case GlobalAction.EditorNudgeRight: + nudgeSelection(1); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } + + /// + /// Nudge the current selection by the specified multiple of beat divisor lengths, + /// based on the timing at the first object in the selection. + /// + /// The direction and count of beat divisor lengths to adjust. + private void nudgeSelection(int amount) + { + var selected = EditorBeatmap.SelectedHitObjects; + + if (selected.Count == 0) + return; + + var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(selected.First().StartTime); + double adjustment = timingPoint.BeatLength / EditorBeatmap.BeatDivisor * amount; + + EditorBeatmap.PerformOnSelection(h => h.StartTime += adjustment); + } } private class TimelineDragBox : DragBox From 30e6ea4291dd95fb87108263df1f677e151d32f4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 18:59:57 +0900 Subject: [PATCH 57/63] Add failing test --- .../TestSceneHoldNoteInput.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 42ea12214f..668487f673 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -324,6 +324,33 @@ namespace osu.Game.Rulesets.Mania.Tests assertTailJudgement(HitResult.Ok); } + [Test] + public void TestZeroLength() + { + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = 1000, + Duration = 0, + Column = 0, + }, + }, + BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo }, + }; + + performTest(new List + { + new ManiaReplayFrame(beatmap.HitObjects[0].StartTime, ManiaAction.Key1), + new ManiaReplayFrame(beatmap.HitObjects[0].GetEndTime() + 1), + }, beatmap); + + AddAssert("hold note hit", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) + .All(j => j.Type.IsHit())); + } + private void assertHeadJudgement(HitResult result) => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result); From 4148d473e3d4c949b312abcbc915f2fd3e99e925 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 19:51:33 +0900 Subject: [PATCH 58/63] Fix hold note crashing with 0 length --- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 828ee7b03e..02829d87bd 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // As the note is being held, adjust the size of the sizing container. This has two effects: // 1. The contained masking container will mask the body and ticks. // 2. The head note will move along with the new "head position" in the container. - if (Head.IsHit && releaseTime == null) + if (Head.IsHit && releaseTime == null && DrawHeight > 0) { // How far past the hit target this hold note is. Always a positive value. float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y); From d20a8694e47264a5d6363550d8f3582d70cb8d10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 19:55:06 +0900 Subject: [PATCH 59/63] Update resources --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 3324af7c51..5b779b803f 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fcd1ed6987..da8163a971 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 34810a3106..dc7a57f905 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From b4f492ca4ce6e171bd3ff9cfd02d5a98140f0666 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Apr 2021 23:06:46 +0900 Subject: [PATCH 60/63] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 7b97103851..140057f9cc 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index b27e00f59d..532dea7ace 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index d07e16ea56..edebefa70e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 0ee73b8e536a37d94d56fcfe4f1878716f5687bf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 23:22:44 +0900 Subject: [PATCH 61/63] Add failing test --- .../StatefulMultiplayerClientTest.cs | 26 +++++++++++++++++++ .../Multiplayer/TestMultiplayerClient.cs | 11 +++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index a2ad37cf4a..377a33b527 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -41,6 +41,32 @@ namespace osu.Game.Tests.NonVisual.Multiplayer checkPlayingUserCount(0); } + [Test] + public void TestPlayingUsersUpdatedOnJoin() + { + AddStep("leave room", () => Client.LeaveRoom()); + AddUntilStep("wait for room part", () => Client.Room == null); + + AddStep("create room initially in gameplay", () => + { + Room.RoomID.Value = null; + Client.RoomSetupAction = room => + { + room.State = MultiplayerRoomState.Playing; + room.Users.Add(new MultiplayerRoomUser(55) + { + User = new User { Id = 55 }, + State = MultiplayerUserState.Playing + }); + }; + + RoomManager.CreateRoom(Room); + }); + + AddUntilStep("wait for room join", () => Client.Room != null); + checkPlayingUserCount(1); + } + private void checkPlayingUserCount(int expectedCount) => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => Client.CurrentMatchPlayingUserIds.Count == expectedCount); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index b5cd3dad02..de77a15da0 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -25,6 +25,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public override IBindable IsConnected => isConnected; private readonly Bindable isConnected = new Bindable(true); + public Action? RoomSetupAction; + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -112,7 +114,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId); - var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) + var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value }; @@ -129,10 +131,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(), PlaylistItemId = apiRoom.Playlist.Last().ID }, - Users = { user }, - Host = user + Users = { localUser }, + Host = localUser }; + RoomSetupAction?.Invoke(room); + RoomSetupAction = null; + return Task.FromResult(room); } From f593d9e42c3efa144dc07df35f504bd15efc41c7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 22 Apr 2021 23:23:43 +0900 Subject: [PATCH 62/63] Fix playing users not being updated on room join --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 2ddc10db0f..c0706b082d 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -144,6 +144,8 @@ namespace osu.Game.Online.Multiplayer Room = joinedRoom; apiRoom = room; defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0; + foreach (var user in joinedRoom.Users) + updateUserPlayingState(user.UserID, user.State); }, cancellationSource.Token).ConfigureAwait(false); // Update room settings. From fbb9cb3f6f6d0a9d6fe0d89aa66ca26b545ec982 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Apr 2021 11:01:48 +0900 Subject: [PATCH 63/63] Fix broken merge resolution --- osu.Game/osu.Game.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index d3d2de4557..75a3e45941 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,6 @@ -