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);