mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 15:22: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.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,
|
||||
|
@ -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
|
||||
|
@ -324,6 +324,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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>
|
||||
/// "Toggle chat focus"
|
||||
/// </summary>
|
||||
|
@ -9,6 +9,16 @@ namespace osu.Game.Localisation
|
||||
{
|
||||
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>
|
||||
/// "Seek backward {0} seconds"
|
||||
/// </summary>
|
||||
|
@ -1,19 +1,17 @@
|
||||
// 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.
|
||||
|
||||
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<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]
|
||||
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
|
||||
|
@ -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<GlobalAction>
|
||||
{
|
||||
public const double BASE_SEEK_AMOUNT = 1000;
|
||||
|
||||
private readonly Func<IBeatmap, IReadOnlyList<Mod>, 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<bool>(OsuSetting.ReplayPlaybackControlsExpanded) }
|
||||
};
|
||||
|
||||
if (GameplayClockContainer is MasterGameplayClockContainer master)
|
||||
@ -92,16 +96,22 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> 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<GlobalAction> e)
|
||||
|
Loading…
Reference in New Issue
Block a user