mirror of
https://github.com/osukey/osukey.git
synced 2025-08-08 00:53:56 +09:00
Merge pull request #17302 from peppy/fix-spectator-seeks
Fix spectator not starting from current player position
This commit is contained in:
@ -26,6 +26,12 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
|
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("reset audio offset", () => localConfig.SetValue(OsuSetting.AudioOffset, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestStartThenElapsedTime()
|
public void TestStartThenElapsedTime()
|
||||||
{
|
{
|
||||||
@ -36,7 +42,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
working.LoadTrack();
|
working.LoadTrack();
|
||||||
|
|
||||||
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
|
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("start clock", () => gameplayClockContainer.Start());
|
AddStep("start clock", () => gameplayClockContainer.Start());
|
||||||
@ -53,7 +59,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
working.LoadTrack();
|
working.LoadTrack();
|
||||||
|
|
||||||
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
|
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("start clock", () => gameplayClockContainer.Start());
|
AddStep("start clock", () => gameplayClockContainer.Start());
|
||||||
@ -73,26 +79,29 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
public void TestSeekPerformsInGameplayTime(
|
public void TestSeekPerformsInGameplayTime(
|
||||||
[Values(1.0, 0.5, 2.0)] double clockRate,
|
[Values(1.0, 0.5, 2.0)] double clockRate,
|
||||||
[Values(0.0, 200.0, -200.0)] double userOffset,
|
[Values(0.0, 200.0, -200.0)] double userOffset,
|
||||||
[Values(false, true)] bool whileStopped)
|
[Values(false, true)] bool whileStopped,
|
||||||
|
[Values(false, true)] bool setAudioOffsetBeforeConstruction)
|
||||||
{
|
{
|
||||||
ClockBackedTestWorkingBeatmap working = null;
|
ClockBackedTestWorkingBeatmap working = null;
|
||||||
GameplayClockContainer gameplayClockContainer = null;
|
GameplayClockContainer gameplayClockContainer = null;
|
||||||
|
|
||||||
|
if (setAudioOffsetBeforeConstruction)
|
||||||
|
AddStep($"preset audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
|
||||||
|
|
||||||
AddStep("create container", () =>
|
AddStep("create container", () =>
|
||||||
{
|
{
|
||||||
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
|
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
|
||||||
working.LoadTrack();
|
working.LoadTrack();
|
||||||
|
|
||||||
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0));
|
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
|
||||||
|
|
||||||
if (whileStopped)
|
gameplayClockContainer.Reset(startClock: !whileStopped);
|
||||||
gameplayClockContainer.Stop();
|
|
||||||
|
|
||||||
gameplayClockContainer.Reset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate)));
|
AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate)));
|
||||||
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
|
|
||||||
|
if (!setAudioOffsetBeforeConstruction)
|
||||||
|
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
|
||||||
|
|
||||||
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
|
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
|
||||||
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));
|
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));
|
||||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSampleHasLifetimeEndWithInitialClockTime()
|
public void TestSampleHasLifetimeEndWithInitialClockTime()
|
||||||
{
|
{
|
||||||
GameplayClockContainer gameplayContainer = null;
|
MasterGameplayClockContainer gameplayContainer = null;
|
||||||
DrawableStoryboardSample sample = null;
|
DrawableStoryboardSample sample = null;
|
||||||
|
|
||||||
AddStep("create container", () =>
|
AddStep("create container", () =>
|
||||||
@ -96,8 +96,11 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
working.LoadTrack();
|
working.LoadTrack();
|
||||||
|
|
||||||
Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true)
|
const double start_time = 1000;
|
||||||
|
|
||||||
|
Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time)
|
||||||
{
|
{
|
||||||
|
StartTime = start_time,
|
||||||
IsPaused = { Value = true },
|
IsPaused = { Value = true },
|
||||||
Child = new FrameStabilityContainer
|
Child = new FrameStabilityContainer
|
||||||
{
|
{
|
||||||
|
@ -56,10 +56,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private double lastFrequency = double.MaxValue;
|
private double lastFrequency = double.MaxValue;
|
||||||
|
|
||||||
protected override void Update()
|
protected override void UpdateAfterChildren()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.UpdateAfterChildren();
|
||||||
|
|
||||||
|
// This must be done in UpdateAfterChildren to allow the gameplay clock to have updated before checking values.
|
||||||
double freq = Beatmap.Value.Track.AggregateFrequency.Value;
|
double freq = Beatmap.Value.Track.AggregateFrequency.Value;
|
||||||
|
|
||||||
FrequencyIncreased |= freq > lastFrequency;
|
FrequencyIncreased |= freq > lastFrequency;
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -36,10 +34,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
BeatmapInfo = { AudioLeadIn = leadIn }
|
BeatmapInfo = { AudioLeadIn = leadIn }
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert($"first frame is {expectedStartTime}", () =>
|
AddStep("check first frame time", () =>
|
||||||
{
|
{
|
||||||
Debug.Assert(player.FirstFrameClockTime != null);
|
Assert.That(player.FirstFrameClockTime, Is.Not.Null);
|
||||||
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
|
Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,10 +57,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
|
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
|
||||||
|
|
||||||
AddAssert($"first frame is {expectedStartTime}", () =>
|
AddStep("check first frame time", () =>
|
||||||
{
|
{
|
||||||
Debug.Assert(player.FirstFrameClockTime != null);
|
Assert.That(player.FirstFrameClockTime, Is.Not.Null);
|
||||||
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
|
Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,10 +95,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
|
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
|
||||||
|
|
||||||
AddAssert($"first frame is {expectedStartTime}", () =>
|
AddStep("check first frame time", () =>
|
||||||
{
|
{
|
||||||
Debug.Assert(player.FirstFrameClockTime != null);
|
Assert.That(player.FirstFrameClockTime, Is.Not.Null);
|
||||||
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
|
Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,16 +464,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private class TestMultiSpectatorScreen : MultiSpectatorScreen
|
private class TestMultiSpectatorScreen : MultiSpectatorScreen
|
||||||
{
|
{
|
||||||
private readonly double? gameplayStartTime;
|
private readonly double? startTime;
|
||||||
|
|
||||||
public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? gameplayStartTime = null)
|
public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? startTime = null)
|
||||||
: base(room, users)
|
: base(room, users)
|
||||||
{
|
{
|
||||||
this.gameplayStartTime = gameplayStartTime;
|
this.startTime = startTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap)
|
protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap)
|
||||||
=> new MasterGameplayClockContainer(beatmap, gameplayStartTime ?? 0, gameplayStartTime.HasValue);
|
=> new MasterGameplayClockContainer(beatmap, 0) { StartTime = startTime ?? 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -495,17 +495,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
|
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
|
||||||
|
|
||||||
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
|
AddAssert("Mods match current item",
|
||||||
|
() => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
|
||||||
|
|
||||||
AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() });
|
AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() });
|
||||||
|
|
||||||
AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
|
AddAssert("Mods don't match current item",
|
||||||
|
() => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
|
||||||
|
|
||||||
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||||
|
|
||||||
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
||||||
|
|
||||||
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
|
AddAssert("Mods match current item",
|
||||||
|
() => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -665,6 +668,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
|
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGameplayDoesntStartWithNonLoadedUser()
|
||||||
|
{
|
||||||
|
createRoom(() => new Room
|
||||||
|
{
|
||||||
|
Name = { Value = "Test Room" },
|
||||||
|
Playlist =
|
||||||
|
{
|
||||||
|
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
|
||||||
|
{
|
||||||
|
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pressReadyButton();
|
||||||
|
|
||||||
|
AddStep("join other user and ready", () =>
|
||||||
|
{
|
||||||
|
multiplayerClient.AddUser(new APIUser { Id = 1234 });
|
||||||
|
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start match", () =>
|
||||||
|
{
|
||||||
|
multiplayerClient.StartMatch();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player);
|
||||||
|
|
||||||
|
AddWaitStep("wait some", 20);
|
||||||
|
|
||||||
|
AddAssert("ensure gameplay hasn't started", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.IsRunning == false);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRoomSettingsReQueriedWhenJoiningRoom()
|
public void TestRoomSettingsReQueriedWhenJoiningRoom()
|
||||||
{
|
{
|
||||||
|
@ -133,6 +133,11 @@ namespace osu.Game.Rulesets.UI
|
|||||||
p.NewResult += (_, r) => NewResult?.Invoke(r);
|
p.NewResult += (_, r) => NewResult?.Invoke(r);
|
||||||
p.RevertResult += (_, r) => RevertResult?.Invoke(r);
|
p.RevertResult += (_, r) => RevertResult?.Invoke(r);
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
IsPaused.ValueChanged += paused =>
|
IsPaused.ValueChanged += paused =>
|
||||||
{
|
{
|
||||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.GameplayTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
||||||
=> new MasterGameplayClockContainer(beatmap, editorState.Time, true);
|
=> new MasterGameplayClockContainer(beatmap, gameplayStart) { StartTime = editorState.Time };
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
|
@ -17,8 +17,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
Bindable<bool> WaitingOnFrames { get; }
|
Bindable<bool> WaitingOnFrames { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this clock is resynchronising to the master clock.
|
/// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Of note, this will be false if this clock is *ahead* of the master clock.
|
||||||
|
/// </remarks>
|
||||||
bool IsCatchingUp { get; set; }
|
bool IsCatchingUp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -55,12 +55,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
public SpectatorGameplayClockContainer([NotNull] IClock sourceClock)
|
public SpectatorGameplayClockContainer([NotNull] IClock sourceClock)
|
||||||
: base(sourceClock)
|
: base(sourceClock)
|
||||||
{
|
{
|
||||||
// the container should initially be in a stopped state until the catch-up clock is started by the sync manager.
|
|
||||||
Stop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
|
// The SourceClock here is always a CatchUpSpectatorPlayerClock.
|
||||||
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay.
|
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay.
|
||||||
if (SourceClock.IsRunning)
|
if (SourceClock.IsRunning)
|
||||||
Start();
|
Start();
|
||||||
|
@ -164,7 +164,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
masterClockContainer.Reset();
|
masterClockContainer.Reset();
|
||||||
masterClockContainer.Stop();
|
|
||||||
|
|
||||||
syncManager.ReadyToStart += onReadyToStart;
|
syncManager.ReadyToStart += onReadyToStart;
|
||||||
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
|
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
|
||||||
@ -198,8 +197,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
.DefaultIfEmpty(0)
|
.DefaultIfEmpty(0)
|
||||||
.Min();
|
.Min();
|
||||||
|
|
||||||
masterClockContainer.Seek(startTime);
|
masterClockContainer.StartTime = startTime;
|
||||||
masterClockContainer.Start();
|
masterClockContainer.Reset(true);
|
||||||
|
|
||||||
// Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it.
|
// Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it.
|
||||||
canStartMasterClock = true;
|
canStartMasterClock = true;
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Screens.Play
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether gameplay is paused.
|
/// Whether gameplay is paused.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly BindableBool IsPaused = new BindableBool();
|
public readonly BindableBool IsPaused = new BindableBool(true);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The adjustable source 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.
|
||||||
@ -41,6 +41,15 @@ namespace osu.Game.Screens.Play
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action OnSeek;
|
public event Action OnSeek;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time from which the clock should start. Will be seeked to on calling <see cref="Reset"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If not set, a value of zero will be used.
|
||||||
|
/// Importantly, the value will be inferred from the current ruleset in <see cref="MasterGameplayClockContainer"/> unless specified.
|
||||||
|
/// </remarks>
|
||||||
|
public double? StartTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="GameplayClockContainer"/>.
|
/// Creates a new <see cref="GameplayClockContainer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -106,16 +115,17 @@ namespace osu.Game.Screens.Play
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay.
|
/// Resets this <see cref="GameplayClockContainer"/> and the source to an initial state ready for gameplay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Reset()
|
/// <param name="startClock">Whether to start the clock immediately, if not already started.</param>
|
||||||
|
public void Reset(bool startClock = false)
|
||||||
{
|
{
|
||||||
ensureSourceClockSet();
|
|
||||||
Seek(0);
|
|
||||||
|
|
||||||
// Manually stop the source in order to not affect the IsPaused state.
|
// Manually stop the source in order to not affect the IsPaused state.
|
||||||
AdjustableSource.Stop();
|
AdjustableSource.Stop();
|
||||||
|
|
||||||
if (!IsPaused.Value)
|
if (!IsPaused.Value || startClock)
|
||||||
Start();
|
Start();
|
||||||
|
|
||||||
|
ensureSourceClockSet();
|
||||||
|
Seek(StartTime ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -46,36 +46,36 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset;
|
private double totalAppliedOffset => userBeatmapOffsetClock.RateAdjustedOffset + userGlobalOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset;
|
||||||
|
|
||||||
private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1);
|
private readonly BindableDouble pauseFreqAdjust = new BindableDouble(); // Important that this starts at zero, matching the paused state of the clock.
|
||||||
|
|
||||||
private readonly WorkingBeatmap beatmap;
|
private readonly WorkingBeatmap beatmap;
|
||||||
private readonly double gameplayStartTime;
|
|
||||||
private readonly bool startAtGameplayStart;
|
|
||||||
private readonly double firstHitObjectTime;
|
|
||||||
|
|
||||||
private HardwareCorrectionOffsetClock userGlobalOffsetClock;
|
private HardwareCorrectionOffsetClock userGlobalOffsetClock;
|
||||||
private HardwareCorrectionOffsetClock userBeatmapOffsetClock;
|
private HardwareCorrectionOffsetClock userBeatmapOffsetClock;
|
||||||
private HardwareCorrectionOffsetClock platformOffsetClock;
|
private HardwareCorrectionOffsetClock platformOffsetClock;
|
||||||
private MasterGameplayClock masterGameplayClock;
|
private MasterGameplayClock masterGameplayClock;
|
||||||
private Bindable<double> userAudioOffset;
|
private Bindable<double> userAudioOffset;
|
||||||
private double startOffset;
|
|
||||||
|
|
||||||
private IDisposable beatmapOffsetSubscription;
|
private IDisposable beatmapOffsetSubscription;
|
||||||
|
|
||||||
|
private readonly double skipTargetTime;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private RealmAccess realm { get; set; }
|
private RealmAccess realm { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; }
|
private OsuConfigManager config { get; set; }
|
||||||
|
|
||||||
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false)
|
/// <summary>
|
||||||
|
/// Create a new master gameplay clock container.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="beatmap">The beatmap to be used for time and metadata references.</param>
|
||||||
|
/// <param name="skipTargetTime">The latest time which should be used when introducing gameplay. Will be used when skipping forward.</param>
|
||||||
|
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime)
|
||||||
: base(beatmap.Track)
|
: base(beatmap.Track)
|
||||||
{
|
{
|
||||||
this.beatmap = beatmap;
|
this.beatmap = beatmap;
|
||||||
this.gameplayStartTime = gameplayStartTime;
|
this.skipTargetTime = skipTargetTime;
|
||||||
this.startAtGameplayStart = startAtGameplayStart;
|
|
||||||
|
|
||||||
firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -90,41 +90,67 @@ namespace osu.Game.Screens.Play
|
|||||||
settings => settings.Offset,
|
settings => settings.Offset,
|
||||||
val => userBeatmapOffsetClock.Offset = val);
|
val => userBeatmapOffsetClock.Offset = val);
|
||||||
|
|
||||||
// sane default provided by ruleset.
|
// Reset may have been called externally before LoadComplete.
|
||||||
startOffset = gameplayStartTime;
|
// If it was, and the clock is in a playing state, we want to ensure that it isn't stopped here.
|
||||||
|
bool isStarted = !IsPaused.Value;
|
||||||
|
|
||||||
if (!startAtGameplayStart)
|
// If a custom start time was not specified, calculate the best value to use.
|
||||||
{
|
StartTime ??= findEarliestStartTime();
|
||||||
startOffset = Math.Min(0, startOffset);
|
|
||||||
|
|
||||||
// if a storyboard is present, it may dictate the appropriate start time by having events in negative time space.
|
Reset(startClock: isStarted);
|
||||||
// this is commonly used to display an intro before the audio track start.
|
}
|
||||||
double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime;
|
|
||||||
if (firstStoryboardEvent != null)
|
|
||||||
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.
|
private double findEarliestStartTime()
|
||||||
// this is not available as an option in the live editor but can still be applied via .osu editing.
|
{
|
||||||
if (beatmap.BeatmapInfo.AudioLeadIn > 0)
|
// here we are trying to find the time to start playback from the "zero" point.
|
||||||
startOffset = Math.Min(startOffset, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn);
|
// generally this is either zero, or some point earlier than zero in the case of storyboards, lead-ins etc.
|
||||||
}
|
|
||||||
|
|
||||||
Seek(startOffset);
|
// start with the originally provided latest time (if before zero).
|
||||||
|
double time = Math.Min(0, skipTargetTime);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
time = Math.Min(time, 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.
|
||||||
|
double firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
|
||||||
|
if (beatmap.BeatmapInfo.AudioLeadIn > 0)
|
||||||
|
time = Math.Min(time, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn);
|
||||||
|
|
||||||
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnIsPausedChanged(ValueChangedEvent<bool> isPaused)
|
protected override void OnIsPausedChanged(ValueChangedEvent<bool> isPaused)
|
||||||
{
|
{
|
||||||
// The source is stopped by a frequency fade first.
|
if (IsLoaded)
|
||||||
if (isPaused.NewValue)
|
|
||||||
{
|
{
|
||||||
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ =>
|
// During normal operation, the source is stopped after performing a frequency ramp.
|
||||||
|
if (isPaused.NewValue)
|
||||||
{
|
{
|
||||||
if (IsPaused.Value == isPaused.NewValue)
|
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ =>
|
||||||
AdjustableSource.Stop();
|
{
|
||||||
});
|
if (IsPaused.Value == isPaused.NewValue)
|
||||||
|
AdjustableSource.Stop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
|
{
|
||||||
|
if (isPaused.NewValue)
|
||||||
|
AdjustableSource.Stop();
|
||||||
|
|
||||||
|
// If not yet loaded, we still want to ensure relevant state is correct, as it is used for offset calculations.
|
||||||
|
pauseFreqAdjust.Value = isPaused.NewValue ? 0 : 1;
|
||||||
|
|
||||||
|
// We must also process underlying gameplay clocks to update rate-adjusted offsets with the new frequency adjustment.
|
||||||
|
// Without doing this, an initial seek may be performed with the wrong offset.
|
||||||
|
GameplayClock.UnderlyingClock.ProcessFrame();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Start()
|
public override void Start()
|
||||||
@ -152,10 +178,10 @@ namespace osu.Game.Screens.Play
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Skip()
|
public void Skip()
|
||||||
{
|
{
|
||||||
if (GameplayClock.CurrentTime > gameplayStartTime - MINIMUM_SKIP_TIME)
|
if (GameplayClock.CurrentTime > skipTargetTime - MINIMUM_SKIP_TIME)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
double skipTarget = gameplayStartTime - MINIMUM_SKIP_TIME;
|
double skipTarget = skipTargetTime - MINIMUM_SKIP_TIME;
|
||||||
|
|
||||||
if (GameplayClock.CurrentTime < 0 && skipTarget > 6000)
|
if (GameplayClock.CurrentTime < 0 && skipTarget > 6000)
|
||||||
// double skip exception for storyboards with very long intros
|
// double skip exception for storyboards with very long intros
|
||||||
@ -164,12 +190,6 @@ namespace osu.Game.Screens.Play
|
|||||||
Seek(skipTarget);
|
Seek(skipTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Reset()
|
|
||||||
{
|
|
||||||
base.Reset();
|
|
||||||
Seek(startOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override GameplayClock CreateGameplayClock(IFrameBasedClock source)
|
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.
|
// Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
|
||||||
@ -278,7 +298,6 @@ namespace osu.Game.Screens.Play
|
|||||||
private class MasterGameplayClock : GameplayClock
|
private class MasterGameplayClock : GameplayClock
|
||||||
{
|
{
|
||||||
public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>();
|
public readonly List<Bindable<double>> MutableNonGameplayAdjustments = new List<Bindable<double>>();
|
||||||
|
|
||||||
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
|
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
|
||||||
|
|
||||||
public MasterGameplayClock(FramedOffsetClock underlyingClock)
|
public MasterGameplayClock(FramedOffsetClock underlyingClock)
|
||||||
|
@ -607,30 +607,25 @@ namespace osu.Game.Screens.Play
|
|||||||
private ScheduledDelegate frameStablePlaybackResetDelegate;
|
private ScheduledDelegate frameStablePlaybackResetDelegate;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seeks to a specific time in gameplay, bypassing frame stability.
|
/// Specify and seek to a custom start time from which gameplay should be observed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Intermediate hitobject judgements may not be applied or reverted correctly during this seek.
|
/// This performs a non-frame-stable seek. Intermediate hitobject judgements may not be applied or reverted correctly during this seek.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="time">The destination time to seek to.</param>
|
/// <param name="time">The destination time to seek to.</param>
|
||||||
internal void NonFrameStableSeek(double time)
|
protected void SetGameplayStartTime(double time)
|
||||||
{
|
{
|
||||||
// TODO: This schedule should not be required and is a temporary hotfix.
|
if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
|
||||||
// See https://github.com/ppy/osu/issues/17267 for the issue.
|
frameStablePlaybackResetDelegate.RunTask();
|
||||||
// See https://github.com/ppy/osu/pull/17302 for a better fix which needs some more time.
|
|
||||||
ScheduleAfterChildren(() =>
|
|
||||||
{
|
|
||||||
if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
|
|
||||||
frameStablePlaybackResetDelegate.RunTask();
|
|
||||||
|
|
||||||
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
|
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
|
||||||
DrawableRuleset.FrameStablePlayback = false;
|
DrawableRuleset.FrameStablePlayback = false;
|
||||||
|
|
||||||
Seek(time);
|
GameplayClockContainer.StartTime = time;
|
||||||
|
GameplayClockContainer.Reset();
|
||||||
|
|
||||||
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
|
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
|
||||||
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
|
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -987,7 +982,7 @@ namespace osu.Game.Screens.Play
|
|||||||
if (GameplayClockContainer.GameplayClock.IsRunning)
|
if (GameplayClockContainer.GameplayClock.IsRunning)
|
||||||
throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running");
|
throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running");
|
||||||
|
|
||||||
GameplayClockContainer.Reset();
|
GameplayClockContainer.Reset(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnSuspending(IScreen next)
|
public override void OnSuspending(IScreen next)
|
||||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isFirstBundle && score.Replay.Frames.Count > 0)
|
if (isFirstBundle && score.Replay.Frames.Count > 0)
|
||||||
NonFrameStableSeek(score.Replay.Frames[0].Time);
|
SetGameplayStartTime(score.Replay.Frames[0].Time);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Score CreateScore(IBeatmap beatmap) => score;
|
protected override Score CreateScore(IBeatmap beatmap) => score;
|
||||||
|
Reference in New Issue
Block a user