mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 16:02:55 +08:00
Merge pull request #26607 from peppy/replay-seek-single-frame
Add ability to step forward/backwards single frames when watching replays
This commit is contained in:
commit
daa9279a23
@ -142,6 +142,7 @@ namespace osu.Game.Configuration
|
|||||||
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
|
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
|
||||||
SetDefault(OsuSetting.KeyOverlay, false);
|
SetDefault(OsuSetting.KeyOverlay, false);
|
||||||
SetDefault(OsuSetting.ReplaySettingsOverlay, true);
|
SetDefault(OsuSetting.ReplaySettingsOverlay, true);
|
||||||
|
SetDefault(OsuSetting.ReplayPlaybackControlsExpanded, true);
|
||||||
SetDefault(OsuSetting.GameplayLeaderboard, true);
|
SetDefault(OsuSetting.GameplayLeaderboard, true);
|
||||||
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
|
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
|
||||||
|
|
||||||
@ -421,6 +422,7 @@ namespace osu.Game.Configuration
|
|||||||
ProfileCoverExpanded,
|
ProfileCoverExpanded,
|
||||||
EditorLimitedDistanceSnap,
|
EditorLimitedDistanceSnap,
|
||||||
ReplaySettingsOverlay,
|
ReplaySettingsOverlay,
|
||||||
|
ReplayPlaybackControlsExpanded,
|
||||||
AutomaticallyDownloadMissingBeatmaps,
|
AutomaticallyDownloadMissingBeatmaps,
|
||||||
EditorShowSpeedChanges,
|
EditorShowSpeedChanges,
|
||||||
TouchDisableGameplayTaps,
|
TouchDisableGameplayTaps,
|
||||||
|
@ -170,6 +170,8 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay),
|
new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay),
|
||||||
new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward),
|
new KeyBinding(InputKey.Left, GlobalAction.SeekReplayBackward),
|
||||||
new KeyBinding(InputKey.Right, GlobalAction.SeekReplayForward),
|
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),
|
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -411,7 +413,13 @@ namespace osu.Game.Input.Bindings
|
|||||||
IncreaseOffset,
|
IncreaseOffset,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseOffset))]
|
[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
|
public enum GlobalActionCategory
|
||||||
|
@ -324,6 +324,16 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString SeekReplayBackward => new TranslatableString(getKey(@"seek_replay_backward"), @"Seek replay backward");
|
public static LocalisableString SeekReplayBackward => new TranslatableString(getKey(@"seek_replay_backward"), @"Seek replay backward");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Seek replay forward one frame"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString StepReplayForward => new TranslatableString(getKey(@"step_replay_forward"), @"Seek replay forward one frame");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Step replay backward one frame"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString StepReplayBackward => new TranslatableString(getKey(@"step_replay_backward"), @"Step replay backward one frame");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Toggle chat focus"
|
/// "Toggle chat focus"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -9,6 +9,16 @@ namespace osu.Game.Localisation
|
|||||||
{
|
{
|
||||||
private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings";
|
private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Step backward one frame"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString StepBackward => new TranslatableString(getKey(@"step_backward_frame"), @"Step backward one frame");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Step forward one frame"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString StepForward => new TranslatableString(getKey(@"step_forward_frame"), @"Step forward one frame");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Seek backward {0} seconds"
|
/// "Seek backward {0} seconds"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
// 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;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Screens.Edit.Timing;
|
using osu.Game.Screens.Edit.Timing;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osu.Game.Localisation;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.PlayerSettings
|
namespace osu.Game.Screens.Play.PlayerSettings
|
||||||
{
|
{
|
||||||
@ -28,26 +26,28 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
|||||||
Precision = 0.01,
|
Precision = 0.01,
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly PlayerSliderBar<double> rateSlider;
|
private PlayerSliderBar<double> rateSlider = null!;
|
||||||
|
|
||||||
private readonly OsuSpriteText multiplierText;
|
private OsuSpriteText multiplierText = null!;
|
||||||
|
|
||||||
private readonly BindableBool isPaused = new BindableBool();
|
private readonly IBindable<bool> isPaused = new BindableBool();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private GameplayClockContainer? gameplayClock { get; set; }
|
private ReplayPlayer replayPlayer { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private GameplayState? gameplayState { get; set; }
|
private GameplayClockContainer gameplayClock { get; set; } = null!;
|
||||||
|
|
||||||
|
private IconButton pausePlay = null!;
|
||||||
|
|
||||||
public PlaybackSettings()
|
public PlaybackSettings()
|
||||||
: base("playback")
|
: base("playback")
|
||||||
{
|
{
|
||||||
const double seek_amount = 5000;
|
}
|
||||||
const double seek_fast_amount = 10000;
|
|
||||||
|
|
||||||
IconButton play;
|
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new FillFlowContainer
|
new FillFlowContainer
|
||||||
@ -71,50 +71,62 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Icon = FontAwesome.Solid.FastBackward,
|
Icon = FontAwesome.Solid.FastBackward,
|
||||||
Action = () => seek(-1, seek_fast_amount),
|
Action = () => replayPlayer.SeekInDirection(-10),
|
||||||
TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_fast_amount / 1000),
|
TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(10 * ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
|
||||||
},
|
},
|
||||||
new SeekButton
|
new SeekButton
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Icon = FontAwesome.Solid.Backward,
|
Icon = FontAwesome.Solid.Backward,
|
||||||
Action = () => seek(-1, seek_amount),
|
Action = () => replayPlayer.SeekInDirection(-1),
|
||||||
TooltipText = PlayerSettingsOverlayStrings.SeekBackwardSeconds(seek_amount / 1000),
|
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,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Scale = new Vector2(1.4f),
|
Scale = new Vector2(1.4f),
|
||||||
IconScale = new Vector2(1.4f),
|
IconScale = new Vector2(1.4f),
|
||||||
Icon = FontAwesome.Regular.PlayCircle,
|
|
||||||
Action = () =>
|
Action = () =>
|
||||||
{
|
{
|
||||||
if (gameplayClock != null)
|
if (gameplayClock.IsRunning)
|
||||||
{
|
gameplayClock.Stop();
|
||||||
if (gameplayClock.IsRunning)
|
else
|
||||||
gameplayClock.Stop();
|
gameplayClock.Start();
|
||||||
else
|
|
||||||
gameplayClock.Start();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new SeekButton
|
new SeekButton
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Icon = FontAwesome.Solid.StepForward,
|
||||||
|
Action = () => replayPlayer.StepFrame(1),
|
||||||
|
TooltipText = PlayerSettingsOverlayStrings.StepForward,
|
||||||
|
},
|
||||||
|
new SeekButton
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Icon = FontAwesome.Solid.Forward,
|
Icon = FontAwesome.Solid.Forward,
|
||||||
Action = () => seek(1, seek_amount),
|
Action = () => replayPlayer.SeekInDirection(1),
|
||||||
TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_amount / 1000),
|
TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(ReplayPlayer.BASE_SEEK_AMOUNT / 1000),
|
||||||
},
|
},
|
||||||
new SeekButton
|
new SeekButton
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Icon = FontAwesome.Solid.FastForward,
|
Icon = FontAwesome.Solid.FastForward,
|
||||||
Action = () => seek(1, seek_fast_amount),
|
Action = () => replayPlayer.SeekInDirection(10),
|
||||||
TooltipText = PlayerSettingsOverlayStrings.SeekForwardSeconds(seek_fast_amount / 1000),
|
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()
|
protected override void LoadComplete()
|
||||||
@ -168,8 +160,20 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
|||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true);
|
rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.00}x", true);
|
||||||
|
|
||||||
if (gameplayClock != null)
|
isPaused.BindTo(gameplayClock.IsPaused);
|
||||||
isPaused.BindTarget = 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
|
private partial class SeekButton : IconButton
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -22,8 +23,11 @@ using osu.Game.Users;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.Play
|
namespace osu.Game.Screens.Play
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public partial class ReplayPlayer : Player, IKeyBindingHandler<GlobalAction>
|
public partial class ReplayPlayer : Player, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
|
public const double BASE_SEEK_AMOUNT = 1000;
|
||||||
|
|
||||||
private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore;
|
private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore;
|
||||||
|
|
||||||
private readonly bool replayIsFailedScore;
|
private readonly bool replayIsFailedScore;
|
||||||
@ -52,7 +56,7 @@ namespace osu.Game.Screens.Play
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(OsuConfigManager config)
|
||||||
{
|
{
|
||||||
if (!LoadedBeatmapSuccessfully)
|
if (!LoadedBeatmapSuccessfully)
|
||||||
return;
|
return;
|
||||||
@ -60,7 +64,7 @@ namespace osu.Game.Screens.Play
|
|||||||
var playbackSettings = new PlaybackSettings
|
var playbackSettings = new PlaybackSettings
|
||||||
{
|
{
|
||||||
Depth = float.MaxValue,
|
Depth = float.MaxValue,
|
||||||
Expanded = { Value = false }
|
Expanded = { BindTarget = config.GetBindable<bool>(OsuSetting.ReplayPlaybackControlsExpanded) }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (GameplayClockContainer is MasterGameplayClockContainer master)
|
if (GameplayClockContainer is MasterGameplayClockContainer master)
|
||||||
@ -92,16 +96,22 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
const double keyboard_seek_amount = 5000;
|
|
||||||
|
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
|
case GlobalAction.StepReplayBackward:
|
||||||
|
StepFrame(-1);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case GlobalAction.StepReplayForward:
|
||||||
|
StepFrame(1);
|
||||||
|
return true;
|
||||||
|
|
||||||
case GlobalAction.SeekReplayBackward:
|
case GlobalAction.SeekReplayBackward:
|
||||||
keyboardSeek(-1);
|
SeekInDirection(-1);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GlobalAction.SeekReplayForward:
|
case GlobalAction.SeekReplayForward:
|
||||||
keyboardSeek(1);
|
SeekInDirection(1);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case GlobalAction.TogglePauseReplay:
|
case GlobalAction.TogglePauseReplay:
|
||||||
@ -113,13 +123,28 @@ namespace osu.Game.Screens.Play
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void keyboardSeek(int direction)
|
public void StepFrame(int direction)
|
||||||
{
|
{
|
||||||
double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.GetLastObjectTime());
|
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<GlobalAction> e)
|
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||||
|
Loading…
Reference in New Issue
Block a user