diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index c05831d043..6b2cb4ee74 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -142,6 +142,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
SetDefault(OsuSetting.KeyOverlay, false);
SetDefault(OsuSetting.ReplaySettingsOverlay, true);
+ SetDefault(OsuSetting.ReplayPlaybackControlsExpanded, true);
SetDefault(OsuSetting.GameplayLeaderboard, true);
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
@@ -421,6 +422,7 @@ namespace osu.Game.Configuration
ProfileCoverExpanded,
EditorLimitedDistanceSnap,
ReplaySettingsOverlay,
+ ReplayPlaybackControlsExpanded,
AutomaticallyDownloadMissingBeatmaps,
EditorShowSpeedChanges,
TouchDisableGameplayTaps,
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 5a39c02185..436334cfe1 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -170,6 +170,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay),
new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward),
new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward),
+ new KeyBinding(InputKey.Comma, GlobalAction.StepReplayBackward),
+ new KeyBinding(InputKey.Period, GlobalAction.StepReplayForward),
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings),
};
@@ -411,7 +413,13 @@ namespace osu.Game.Input.Bindings
IncreaseOffset,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))]
- DecreaseOffset
+ DecreaseOffset,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayForward))]
+ StepReplayForward,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))]
+ StepReplayBackward,
}
public enum GlobalActionCategory
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index ca27d0ff95..703e0ff1ca 100644
--- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
+++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
@@ -324,6 +324,16 @@ namespace osu.Game.Localisation
///
public static LocalisableString SeekReplayBackward => new TranslatableString(getKey(@"seek_replay_backward"), @"Seek replay backward");
+ ///
+ /// "Seek replay forward one frame"
+ ///
+ public static LocalisableString StepReplayForward => new TranslatableString(getKey(@"step_replay_forward"), @"Seek replay forward one frame");
+
+ ///
+ /// "Step replay backward one frame"
+ ///
+ public static LocalisableString StepReplayBackward => new TranslatableString(getKey(@"step_replay_backward"), @"Step replay backward one frame");
+
///
/// "Toggle chat focus"
///
diff --git a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs
index 1aedd9fc5b..60874da561 100644
--- a/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs
+++ b/osu.Game/Localisation/PlayerSettingsOverlayStrings.cs
@@ -9,6 +9,16 @@ namespace osu.Game.Localisation
{
private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings";
+ ///
+ /// "Step backward one frame"
+ ///
+ public static LocalisableString StepBackward => new TranslatableString(getKey(@"step_backward_frame"), @"Step backward one frame");
+
+ ///
+ /// "Step forward one frame"
+ ///
+ public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame");
+
///
/// "Seek backward {0} seconds"
///
diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs
index 44cfa8d811..b3d07421ed 100644
--- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs
@@ -1,19 +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 System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Localisation;
using osu.Game.Screens.Edit.Timing;
using osuTK;
-using osu.Game.Localisation;
namespace osu.Game.Screens.Play.PlayerSettings
{
@@ -28,26 +26,28 @@ namespace osu.Game.Screens.Play.PlayerSettings
Precision = 0.01,
};
- private readonly PlayerSliderBar rateSlider;
+ private PlayerSliderBar rateSlider = null!;
- private readonly OsuSpriteText multiplierText;
+ private OsuSpriteText multiplierText = null!;
- private readonly BindableBool isPaused = new BindableBool();
+ private readonly IBindable isPaused = new BindableBool();
[Resolved]
- private GameplayClockContainer? gameplayClock { get; set; }
+ private ReplayPlayer replayPlayer { get; set; } = null!;
[Resolved]
- private GameplayState? gameplayState { get; set; }
+ private GameplayClockContainer gameplayClock { get; set; } = null!;
+
+ private IconButton pausePlay = null!;
public PlaybackSettings()
: base("playback")
{
- const double seek_amount = 5000;
- const double seek_fast_amount = 10000;
-
- IconButton play;
+ }
+ [BackgroundDependencyLoader]
+ private void load()
+ {
Children = new Drawable[]
{
new FillFlowContainer
@@ -71,50 +71,62 @@ namespace osu.Game.Screens.Play.PlayerSettings
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.FastBackward,
- Action = () => seek(-1, seek_fast_amount),
- TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_fast_amount / 1000),
+ Action = () => replayPlayer.SeekInDirection(-10),
+ TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
},
new SeekButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Backward,
- Action = () => seek(-1, seek_amount),
- TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_amount / 1000),
+ Action = () => replayPlayer.SeekInDirection(-1),
+ TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
},
- play = new IconButton
+ new SeekButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Icon = FontAwesome.Solid.StepBackward,
+ Action = () => replayPlayer.StepFrame(-1),
+ TooltipText = PlayerSettingsOverlayStrings.StepBackward,
+ },
+ pausePlay = new IconButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1.4f),
IconScale = new Vector2(1.4f),
- Icon = FontAwesome.Regular.PlayCircle,
Action = () =>
{
- if (gameplayClock != null)
- {
- if (gameplayClock.IsRunning)
- gameplayClock.Stop();
- else
- gameplayClock.Start();
- }
+ if (gameplayClock.IsRunning)
+ gameplayClock.Stop();
+ else
+ gameplayClock.Start();
},
},
new SeekButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Icon = FontAwesome.Solid.StepForward,
+ Action = () => replayPlayer.StepFrame(1),
+ TooltipText = PlayerSettingsOverlayStrings.StepForward,
+ },
+ new SeekButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.Forward,
- Action = () => seek(1, seek_amount),
- TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_amount / 1000),
+ Action = () => replayPlayer.SeekInDirection(1),
+ TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
},
new SeekButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Solid.FastForward,
- Action = () => seek(1, seek_fast_amount),
- TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_fast_amount / 1000),
+ Action = () => replayPlayer.SeekInDirection(10),
+ TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
},
},
},
@@ -141,26 +153,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
},
},
};
-
- isPaused.BindValueChanged(paused =>
- {
- if (!paused.NewValue)
- {
- play.TooltipText = ToastStrings.PauseTrack;
- play.Icon = FontAwesome.Regular.PauseCircle;
- }
- else
- {
- play.TooltipText = ToastStrings.PlayTrack;
- play.Icon = FontAwesome.Regular.PlayCircle;
- }
- }, true);
-
- void seek(int direction, double amount)
- {
- double target = Math.Clamp((gameplayClock?.CurrentTime ?? 0) + (direction * amount), 0, gameplayState?.Beatmap.GetLastObjectTime() ?? 0);
- gameplayClock?.Seek(target);
- }
}
protected override void LoadComplete()
@@ -168,8 +160,20 @@ namespace osu.Game.Screens.Play.PlayerSettings
base.LoadComplete();
rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true);
- if (gameplayClock != null)
- isPaused.BindTarget = gameplayClock.IsPaused;
+ isPaused.BindTo(gameplayClock.IsPaused);
+ isPaused.BindValueChanged(paused =>
+ {
+ if (!paused.NewValue)
+ {
+ pausePlay.TooltipText = ToastStrings.PauseTrack;
+ pausePlay.Icon = FontAwesome.Regular.PauseCircle;
+ }
+ else
+ {
+ pausePlay.TooltipText = ToastStrings.PlayTrack;
+ pausePlay.Icon = FontAwesome.Regular.PlayCircle;
+ }
+ }, true);
}
private partial class SeekButton : IconButton
diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs
index 805f907466..a26a2b9904 100644
--- a/osu.Game/Screens/Play/ReplayPlayer.cs
+++ b/osu.Game/Screens/Play/ReplayPlayer.cs
@@ -12,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@@ -22,8 +23,11 @@ using osu.Game.Users;
namespace osu.Game.Screens.Play
{
+ [Cached]
public partial class ReplayPlayer : Player, IKeyBindingHandler
{
+ public const double BASE_SEEK_AMOUNT = 1000;
+
private readonly Func, Score> createScore;
private readonly bool replayIsFailedScore;
@@ -52,7 +56,7 @@ namespace osu.Game.Screens.Play
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuConfigManager config)
{
if (!LoadedBeatmapSuccessfully)
return;
@@ -60,7 +64,7 @@ namespace osu.Game.Screens.Play
var playbackSettings = new PlaybackSettings
{
Depth = float.MaxValue,
- Expanded = { Value = false }
+ Expanded = { BindTarget = config.GetBindable(OsuSetting.ReplayPlaybackControlsExpanded) }
};
if (GameplayClockContainer is MasterGameplayClockContainer master)
@@ -92,16 +96,22 @@ namespace osu.Game.Screens.Play
public bool OnPressed(KeyBindingPressEvent e)
{
- const double keyboard_seek_amount = 5000;
-
switch (e.Action)
{
+ case GlobalAction.StepReplayBackward:
+ StepFrame(-1);
+ return true;
+
+ case GlobalAction.StepReplayForward:
+ StepFrame(1);
+ return true;
+
case GlobalAction.SeekReplayBackward:
- keyboardSeek(-1);
+ SeekInDirection(-1);
return true;
case GlobalAction.SeekReplayForward:
- keyboardSeek(1);
+ SeekInDirection(1);
return true;
case GlobalAction.TogglePauseReplay:
@@ -113,13 +123,28 @@ namespace osu.Game.Screens.Play
}
return false;
+ }
- void keyboardSeek(int direction)
- {
- double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.GetLastObjectTime());
+ public void StepFrame(int direction)
+ {
+ GameplayClockContainer.Stop();
- Seek(target);
- }
+ var frames = GameplayState.Score.Replay.Frames;
+
+ if (frames.Count == 0)
+ return;
+
+ GameplayClockContainer.Seek(direction < 0
+ ? (frames.LastOrDefault(f => f.Time < GameplayClockContainer.CurrentTime) ?? frames.First()).Time
+ : (frames.FirstOrDefault(f => f.Time > GameplayClockContainer.CurrentTime) ?? frames.Last()).Time
+ );
+ }
+
+ public void SeekInDirection(float amount)
+ {
+ double target = Math.Clamp(GameplayClockContainer.CurrentTime + amount * BASE_SEEK_AMOUNT, 0, GameplayState.Beatmap.GetLastObjectTime());
+
+ Seek(target);
}
public void OnReleased(KeyBindingReleaseEvent e)