diff --git a/osu-framework b/osu-framework index cc50d1251b..dcbd7a0b6f 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit cc50d1251b00876331691ea2ce7ed18174e4eded +Subproject commit dcbd7a0b6f536f6aadf13a720db40a1d76bf52e2 diff --git a/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs b/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs index 7c40d21512..6d8aac1d09 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs @@ -38,9 +38,9 @@ namespace osu.Desktop.VisualTests.Tests Origin = Anchor.TopLeft, }); - AddStep("Toggle Bar", progress.ToggleBar); + AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking); AddWaitStep(5); - AddStep("Toggle Bar", progress.ToggleBar); + AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking); AddWaitStep(2); AddRepeatStep("New Values", displayNewValues, 5); diff --git a/osu.Game/Rulesets/UI/HitRenderer.cs b/osu.Game/Rulesets/UI/HitRenderer.cs index a3a806b6a7..25d8bae205 100644 --- a/osu.Game/Rulesets/UI/HitRenderer.cs +++ b/osu.Game/Rulesets/UI/HitRenderer.cs @@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.UI where TObject : HitObject { /// - /// The Beatmap + /// The Beatmap /// public Beatmap Beatmap; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2668c61eee..37b4cf5b45 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play public Action RestartRequested; - public bool IsPaused => !interpolatedSourceClock.IsRunning; + public bool IsPaused => !decoupledClock.IsRunning; internal override bool AllowRulesetChange => false; @@ -51,9 +51,9 @@ namespace osu.Game.Screens.Play private bool canPause => ValidForResume && !HasFailed && Time.Current >= lastPauseActionTime + pause_cooldown; - private IAdjustableClock sourceClock; - private OffsetClock offsetClock; - private IFrameBasedClock interpolatedSourceClock; + private IAdjustableClock adjustableSourceClock; + private FramedOffsetClock offsetClock; + private DecoupleableInterpolatingFramedClock decoupledClock; private RulesetInfo ruleset; @@ -70,6 +70,8 @@ namespace osu.Game.Screens.Play private SkipButton skipButton; + private Container hitRendererContainer; + private HudOverlay hudOverlay; private PauseOverlay pauseOverlay; private FailOverlay failOverlay; @@ -87,10 +89,7 @@ namespace osu.Game.Screens.Play if (Beatmap == null) Beatmap = beatmaps.GetWorkingBeatmap(BeatmapInfo, withStoryboard: true); - if ((Beatmap?.Beatmap?.HitObjects.Count ?? 0) == 0) - throw new Exception("No valid objects were found!"); - - if (Beatmap == null) + if (Beatmap?.Beatmap == null) throw new Exception("Beatmap was not loaded"); ruleset = osu?.Ruleset.Value ?? Beatmap.BeatmapInfo.Ruleset; @@ -108,6 +107,9 @@ namespace osu.Game.Screens.Play rulesetInstance = ruleset.CreateInstance(); HitRenderer = rulesetInstance.CreateHitRendererWith(Beatmap); } + + if (!HitRenderer.Objects.Any()) + throw new Exception("Beatmap contains no hit objects!"); } catch (Exception e) { @@ -123,23 +125,31 @@ namespace osu.Game.Screens.Play if (track != null) { audio.Track.SetExclusive(track); - sourceClock = track; + adjustableSourceClock = track; } - sourceClock = (IAdjustableClock)track ?? new StopwatchClock(); - offsetClock = new OffsetClock(sourceClock); + adjustableSourceClock = (IAdjustableClock)track ?? new StopwatchClock(); + + decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + + var firstObjectTime = HitRenderer.Objects.First().StartTime; + decoupledClock.Seek(Math.Min(0, firstObjectTime - Math.Max(Beatmap.Beatmap.TimingInfo.BeatLengthAt(firstObjectTime) * 4, Beatmap.BeatmapInfo.AudioLeadIn))); + decoupledClock.ProcessFrame(); + + offsetClock = new FramedOffsetClock(decoupledClock); userAudioOffset = config.GetBindable(OsuConfig.AudioOffset); userAudioOffset.ValueChanged += v => offsetClock.Offset = v; userAudioOffset.TriggerChange(); - interpolatedSourceClock = new InterpolatingFramedClock(offsetClock); - Schedule(() => { - sourceClock.Reset(); + adjustableSourceClock.Reset(); + foreach (var mod in Beatmap.Mods.Value.OfType()) - mod.ApplyToClock(sourceClock); + mod.ApplyToClock(adjustableSourceClock); + + decoupledClock.ChangeSource(adjustableSourceClock); }); scoreProcessor = HitRenderer.CreateScoreProcessor(); @@ -155,7 +165,9 @@ namespace osu.Game.Screens.Play hudOverlay.BindHitRenderer(HitRenderer); hudOverlay.Progress.Objects = HitRenderer.Objects; - hudOverlay.Progress.AudioClock = interpolatedSourceClock; + hudOverlay.Progress.AudioClock = decoupledClock; + hudOverlay.Progress.AllowSeeking = HitRenderer.HasReplayLoaded; + hudOverlay.Progress.OnSeek = pos => decoupledClock.Seek(pos); //bind HitRenderer to ScoreProcessor and ourselves (for a pass situation) HitRenderer.OnAllJudged += onCompletion; @@ -165,16 +177,20 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { - new Container + hitRendererContainer = new Container { RelativeSizeAxes = Axes.Both, - Clock = interpolatedSourceClock, Children = new Drawable[] { - HitRenderer, - skipButton = new SkipButton + new Container { - Alpha = 0 + RelativeSizeAxes = Axes.Both, + Clock = offsetClock, + Children = new Drawable[] + { + HitRenderer, + skipButton = new SkipButton { Alpha = 0 }, + } }, } }, @@ -224,7 +240,7 @@ namespace osu.Game.Screens.Play skipButton.Action = () => { - sourceClock.Seek(firstHitObject - skip_required_cutoff - fade_time); + decoupledClock.Seek(firstHitObject - skip_required_cutoff - fade_time); skipButton.Action = null; }; @@ -241,7 +257,7 @@ namespace osu.Game.Screens.Play // we want to wait for the source clock to stop so we can be sure all components are in a stable state. if (!IsPaused) { - sourceClock.Stop(); + decoupledClock.Stop(); Schedule(() => Pause(force)); return; @@ -268,7 +284,7 @@ namespace osu.Game.Screens.Play hudOverlay.KeyCounter.IsCounting = true; hudOverlay.Progress.Hide(); pauseOverlay.Hide(); - sourceClock.Start(); + decoupledClock.Start(); } public void Restart() @@ -304,7 +320,7 @@ namespace osu.Game.Screens.Play private void onFail() { - sourceClock.Stop(); + decoupledClock.Stop(); HasFailed = true; failOverlay.Retries = RestartCount; @@ -332,13 +348,12 @@ namespace osu.Game.Screens.Play Delay(750); Schedule(() => { - sourceClock.Start(); + decoupledClock.Start(); initializeSkipButton(); }); - //keep in mind this is using the interpolatedSourceClock so won't be run as early as we may expect. - HitRenderer.Alpha = 0; - HitRenderer.FadeIn(750, EasingTypes.OutQuint); + hitRendererContainer.Alpha = 0; + hitRendererContainer.FadeIn(750, EasingTypes.OutQuint); } protected override void OnSuspending(Screen next) diff --git a/osu.Game/Screens/Play/PlayerInputManager.cs b/osu.Game/Screens/Play/PlayerInputManager.cs index 3ac28898a6..654cde1b75 100644 --- a/osu.Game/Screens/Play/PlayerInputManager.cs +++ b/osu.Game/Screens/Play/PlayerInputManager.cs @@ -36,6 +36,38 @@ namespace osu.Game.Screens.Play Clock = new FramedClock(clock); } + /// + /// Whether we running up-to-date with our parent clock. + /// If not, we will need to keep processing children until we catch up. + /// + private bool requireMoreUpdateLoops; + + /// + /// Whether we in a valid state (ie. should we keep processing children frames). + /// This should be set to false when the replay is, for instance, waiting for future frames to arrive. + /// + private bool validState; + + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState; + + private bool isAttached => replayInputHandler != null && !UseParentState; + + private const int max_catch_up_updates_per_frame = 50; + + public override bool UpdateSubTree() + { + requireMoreUpdateLoops = true; + validState = true; + + int loops = 0; + + while (validState && requireMoreUpdateLoops && loops++ < max_catch_up_updates_per_frame) + if (!base.UpdateSubTree()) + return false; + + return true; + } + protected override void Update() { if (parentClock == null) return; @@ -43,28 +75,26 @@ namespace osu.Game.Screens.Play clock.Rate = parentClock.Rate; clock.IsRunning = parentClock.IsRunning; - //if a replayHandler is not attached, we should just pass-through. - if (UseParentState || replayInputHandler == null) + if (!isAttached) { clock.CurrentTime = parentClock.CurrentTime; - base.Update(); - return; } - - while (true) + else { double? newTime = replayInputHandler.SetFrameFromTime(parentClock.CurrentTime); if (newTime == null) - //we shouldn't execute for this time value - break; - - if (clock.CurrentTime == parentClock.CurrentTime) - break; + { + // we shouldn't execute for this time value. probably waiting on more replay data. + validState = false; + return; + } clock.CurrentTime = newTime.Value; - base.Update(); } + + requireMoreUpdateLoops = clock.CurrentTime != parentClock.CurrentTime; + base.Update(); } } } diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index 6ad76ae361..ed57dad644 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play private double lastHitTime => ((objects.Last() as IHasEndTime)?.EndTime ?? objects.Last().StartTime) + 1; + private double firstHitTime => objects.First().StartTime; + private IEnumerable objects; public IEnumerable Objects @@ -75,7 +77,7 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomLeft, SeekRequested = delegate (float position) { - OnSeek?.Invoke(position); + OnSeek?.Invoke(firstHitTime + position * (lastHitTime - firstHitTime)); }, }, }; @@ -86,18 +88,28 @@ namespace osu.Game.Screens.Play State = Visibility.Visible; } - private bool barVisible; + private bool allowSeeking; - public void ToggleBar() + public bool AllowSeeking { - barVisible = !barVisible; - updateBarVisibility(); + get + { + return allowSeeking; + } + + set + { + if (allowSeeking == value) return; + + allowSeeking = value; + updateBarVisibility(); + } } private void updateBarVisibility() { - bar.FadeTo(barVisible ? 1 : 0, transition_duration, EasingTypes.In); - MoveTo(new Vector2(0, barVisible ? 0 : bottom_bar_height), transition_duration, EasingTypes.In); + bar.FadeTo(allowSeeking ? 1 : 0, transition_duration, EasingTypes.In); + MoveTo(new Vector2(0, allowSeeking ? 0 : bottom_bar_height), transition_duration, EasingTypes.In); } protected override void PopIn() @@ -118,7 +130,7 @@ namespace osu.Game.Screens.Play if (objects == null) return; - double progress = (AudioClock?.CurrentTime ?? Time.Current) / lastHitTime; + double progress = ((AudioClock?.CurrentTime ?? Time.Current) - firstHitTime) / lastHitTime; bar.UpdatePosition((float)progress); graph.Progress = (int)(graph.ColumnCount * progress);