1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 10:33:22 +08:00

Merge pull request #8428 from peppy/replay-recording

Add local replay support for all rulesets
This commit is contained in:
Dan Balasescu 2020-03-25 22:02:04 +09:00 committed by GitHub
commit 1587d4b26f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 766 additions and 54 deletions

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch
new KeyBinding(InputKey.Shift, CatchAction.Dash),
};
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new CatchModNightcore();

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH;
Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;
@ -56,5 +56,14 @@ namespace osu.Game.Rulesets.Catch.Replays
Actions.Add(CatchAction.MoveLeft);
}
}
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
{
ReplayButtonState state = ReplayButtonState.None;
if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1;
return new LegacyReplayFrame(Time, Position * CatchPlayfield.BASE_WIDTH, null, state);
}
}
}

View File

@ -0,0 +1,26 @@
// 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.Collections.Generic;
using osu.Game.Replays;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class CatchReplayRecorder : ReplayRecorder<CatchAction>
{
private readonly CatchPlayfield playfield;
public CatchReplayRecorder(Replay target, CatchPlayfield playfield)
: base(target)
{
this.playfield = playfield;
}
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<CatchAction> actions, ReplayFrame previousFrame)
=> new CatchReplayFrame(Time.Current, playfield.CatcherArea.MovableCatcher.X, actions.Contains(CatchAction.Dash), previousFrame as CatchReplayFrame);
}
}

View File

@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.UI
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new CatchReplayRecorder(replay, (CatchPlayfield)Playfield);
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();

View File

@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania
public override ISkin CreateLegacySkinProvider(ISkinSource source) => new ManiaLegacySkinTransformer(source);
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new ManiaModNightcore();
@ -118,6 +118,59 @@ namespace osu.Game.Rulesets.Mania
yield return new ManiaModRandom();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
{
var value = base.ConvertToLegacyMods(mods);
foreach (var mod in mods)
{
switch (mod)
{
case ManiaModKey1 _:
value |= LegacyMods.Key1;
break;
case ManiaModKey2 _:
value |= LegacyMods.Key2;
break;
case ManiaModKey3 _:
value |= LegacyMods.Key3;
break;
case ManiaModKey4 _:
value |= LegacyMods.Key4;
break;
case ManiaModKey5 _:
value |= LegacyMods.Key5;
break;
case ManiaModKey6 _:
value |= LegacyMods.Key6;
break;
case ManiaModKey7 _:
value |= LegacyMods.Key7;
break;
case ManiaModKey8 _:
value |= LegacyMods.Key8;
break;
case ManiaModKey9 _:
value |= LegacyMods.Key9;
break;
case ManiaModFadeIn _:
value |= LegacyMods.FadeIn;
break;
}
}
return value;
}
public override IEnumerable<Mod> GetModsFor(ModType type)
{
switch (type)

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Replays
Actions.AddRange(actions);
}
public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
// We don't need to fully convert, just create the converter
var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset());
@ -56,5 +56,42 @@ namespace osu.Game.Rulesets.Mania.Replays
activeColumns >>= 1;
}
}
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
{
int keys = 0;
var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset());
var stage = new StageDefinition { Columns = converter.TargetColumns };
var specialColumns = new List<int>();
for (int i = 0; i < converter.TargetColumns; i++)
{
if (stage.IsSpecialColumn(i))
specialColumns.Add(i);
}
foreach (var action in Actions)
{
switch (action)
{
case ManiaAction.Special1:
keys |= 1 << specialColumns[0];
break;
case ManiaAction.Special2:
keys |= 1 << specialColumns[1];
break;
default:
keys |= 1 << (action - ManiaAction.Key1);
break;
}
}
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
}
}
}

View File

@ -85,5 +85,7 @@ namespace osu.Game.Rulesets.Mania.UI
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new ManiaReplayRecorder(replay);
}
}

View File

@ -0,0 +1,23 @@
// 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.Collections.Generic;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaReplayRecorder : ReplayRecorder<ManiaAction>
{
public ManiaReplayRecorder(Replay replay)
: base(replay)
{
}
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<ManiaAction> actions, ReplayFrame previousFrame)
=> new ManiaReplayFrame(Time.Current, actions.ToArray());
}
}

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu
new KeyBinding(InputKey.MouseRight, OsuAction.RightButton),
};
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new OsuModNightcore();

View File

@ -26,11 +26,23 @@ namespace osu.Game.Rulesets.Osu.Replays
Actions.AddRange(actions);
}
public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
Position = currentFrame.Position;
if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton);
}
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
{
ReplayButtonState state = ReplayButtonState.None;
if (Actions.Contains(OsuAction.LeftButton))
state |= ReplayButtonState.Left1;
if (Actions.Contains(OsuAction.RightButton))
state |= ReplayButtonState.Right1;
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
}
}
}

View File

@ -58,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.UI
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new OsuReplayRecorder(replay);
public override double GameplayStartTime
{
get

View File

@ -0,0 +1,23 @@
// 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.Collections.Generic;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
public class OsuReplayRecorder : ReplayRecorder<OsuAction>
{
public OsuReplayRecorder(Replay replay)
: base(replay)
{
}
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<OsuAction> actions, ReplayFrame previousFrame)
=> new OsuReplayFrame(Time.Current, mousePosition, actions.ToArray());
}
}

View File

@ -23,12 +23,24 @@ namespace osu.Game.Rulesets.Taiko.Replays
Actions.AddRange(actions);
}
public void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{
if (currentFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim);
if (currentFrame.MouseRight2) Actions.Add(TaikoAction.RightRim);
if (currentFrame.MouseLeft1) Actions.Add(TaikoAction.LeftCentre);
if (currentFrame.MouseLeft2) Actions.Add(TaikoAction.RightCentre);
}
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
{
ReplayButtonState state = ReplayButtonState.None;
if (Actions.Contains(TaikoAction.LeftRim)) state |= ReplayButtonState.Right1;
if (Actions.Contains(TaikoAction.RightRim)) state |= ReplayButtonState.Right2;
if (Actions.Contains(TaikoAction.LeftCentre)) state |= ReplayButtonState.Left1;
if (Actions.Contains(TaikoAction.RightCentre)) state |= ReplayButtonState.Left2;
return new LegacyReplayFrame(Time, null, null, state);
}
}
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Taiko
new KeyBinding(InputKey.K, TaikoAction.RightRim),
};
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{
if (mods.HasFlag(LegacyMods.Nightcore))
yield return new TaikoModNightcore();

View File

@ -65,5 +65,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new TaikoReplayRecorder(replay);
}
}

View File

@ -0,0 +1,23 @@
// 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.Collections.Generic;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
public class TaikoReplayRecorder : ReplayRecorder<TaikoAction>
{
public TaikoReplayRecorder(Replay replay)
: base(replay)
{
}
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<TaikoAction> actions, ReplayFrame previousFrame) =>
new TaikoReplayFrame(Time.Current, actions.ToArray());
}
}

View File

@ -10,7 +10,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Input.States;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Graphics.Sprites;
@ -49,7 +48,10 @@ namespace osu.Game.Tests.Gameplay
{
recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Recorder = recorder = new TestReplayRecorder(replay),
Recorder = recorder = new TestReplayRecorder(replay)
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
@ -273,8 +275,8 @@ namespace osu.Game.Tests.Gameplay
{
}
protected override ReplayFrame HandleFrame(InputState state, List<TestAction> pressedActions, ReplayFrame previousFrame) =>
new TestReplayFrame(Time.Current, ToLocalSpace(state.Mouse.Position), pressedActions.ToArray());
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<TestAction> actions, ReplayFrame previousFrame)
=> new TestReplayFrame(Time.Current, mousePosition, actions.ToArray());
}
}
}

View File

@ -0,0 +1,221 @@
// 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.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Game.Graphics.Sprites;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Gameplay
{
public class TestSceneReplayRecording : OsuTestScene
{
private readonly TestRulesetInputManager playbackManager;
private readonly TestRulesetInputManager recordingManager;
public TestSceneReplayRecording()
{
Replay replay = new Replay();
Add(new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
Recorder = new TestReplayRecorder(replay)
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos)
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Recording",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestConsumer()
}
},
}
},
new Drawable[]
{
playbackManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
ReplayInputHandler = new TestFramedReplayInputHandler(replay)
{
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Playback",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestConsumer()
}
},
}
}
}
});
}
protected override void Update()
{
base.Update();
playbackManager.ReplayInputHandler.SetFrameFromTime(Time.Current - 500);
}
}
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>
{
public TestFramedReplayInputHandler(Replay replay)
: base(replay)
{
}
public override List<IInput> GetPendingInputs()
{
return new List<IInput>
{
new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
},
new ReplayState<TestAction>
{
PressedActions = CurrentFrame?.Actions ?? new List<TestAction>()
}
};
}
}
public class TestConsumer : CompositeDrawable, IKeyBindingHandler<TestAction>
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos);
private readonly Box box;
public TestConsumer()
{
Size = new Vector2(30);
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
box = new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
};
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
Position = e.MousePosition;
return base.OnMouseMove(e);
}
public bool OnPressed(TestAction action)
{
box.Colour = Color4.White;
return true;
}
public void OnReleased(TestAction action)
{
box.Colour = Color4.Black;
}
}
public class TestRulesetInputManager : RulesetInputManager<TestAction>
{
public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
: base(ruleset, variant, unique)
{
}
protected override KeyBindingContainer<TestAction> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new TestKeyBindingContainer();
internal class TestKeyBindingContainer : KeyBindingContainer<TestAction>
{
public override IEnumerable<KeyBinding> DefaultKeyBindings => new[]
{
new KeyBinding(InputKey.MouseLeft, TestAction.Down),
};
}
}
public class TestReplayFrame : ReplayFrame
{
public Vector2 Position;
public List<TestAction> Actions = new List<TestAction>();
public TestReplayFrame(double time, Vector2 position, params TestAction[] actions)
: base(time)
{
Position = position;
Actions.AddRange(actions);
}
}
public enum TestAction
{
Down,
}
internal class TestReplayRecorder : ReplayRecorder<TestAction>
{
public TestReplayRecorder(Replay target)
: base(target)
{
}
protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<TestAction> actions, ReplayFrame previousFrame) =>
new TestReplayFrame(Time.Current, mousePosition, actions.ToArray());
}
}

View File

@ -236,8 +236,6 @@ namespace osu.Game.Tests.Scores.IO
}
public override IEnumerable<string> Filenames => new[] { "test_file.osr" };
public override Stream GetUnderlyingStream() => new MemoryStream();
}
}
}

View File

@ -45,7 +45,5 @@ namespace osu.Game.IO.Archives
return buffer;
}
}
public abstract Stream GetUnderlyingStream();
}
}

View File

@ -0,0 +1,30 @@
// 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.Collections.Generic;
using System.IO;
namespace osu.Game.IO.Archives
{
/// <summary>
/// Allows reading a single file from the provided stream.
/// </summary>
public class LegacyByteArrayReader : ArchiveReader
{
private readonly byte[] content;
public LegacyByteArrayReader(byte[] content, string filename)
: base(filename)
{
this.content = content;
}
public override Stream GetStream(string name) => new MemoryStream(content);
public override void Dispose()
{
}
public override IEnumerable<string> Filenames => new[] { Name };
}
}

View File

@ -28,7 +28,5 @@ namespace osu.Game.IO.Archives
}
public override IEnumerable<string> Filenames => Directory.GetFiles(path, "*", SearchOption.AllDirectories).Select(f => f.Replace(path, string.Empty).Trim(Path.DirectorySeparatorChar)).ToArray();
public override Stream GetUnderlyingStream() => null;
}
}

View File

@ -28,7 +28,5 @@ namespace osu.Game.IO.Archives
}
public override IEnumerable<string> Filenames => new[] { Name };
public override Stream GetUnderlyingStream() => null;
}
}

View File

@ -45,7 +45,5 @@ namespace osu.Game.IO.Archives
}
public override IEnumerable<string> Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
public override Stream GetUnderlyingStream() => archiveStream;
}
}

View File

@ -17,6 +17,12 @@ namespace osu.Game.Rulesets.Replays.Types
/// <param name="currentFrame">The <see cref="LegacyReplayFrame"/> to extract values from.</param>
/// <param name="beatmap">The beatmap.</param>
/// <param name="lastFrame">The last post-conversion <see cref="ReplayFrame"/>, used to fill in missing delta information. May be null.</param>
void ConvertFrom(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null);
void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null);
/// <summary>
/// Populates this <see cref="ReplayFrame"/> using values from a <see cref="LegacyReplayFrame"/>.
/// </summary>
/// <param name="beatmap">The beatmap.</param>
LegacyReplayFrame ToLegacy(IBeatmap beatmap);
}
}

View File

@ -42,9 +42,63 @@ namespace osu.Game.Rulesets
/// <summary>
/// Converts mods from legacy enum values. Do not override if you're not a legacy ruleset.
/// </summary>
/// <param name="mods">The legacy enum which will be converted</param>
/// <returns>An enumerable of constructed <see cref="Mod"/>s</returns>
public virtual IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods) => Array.Empty<Mod>();
/// <param name="mods">The legacy enum which will be converted.</param>
/// <returns>An enumerable of constructed <see cref="Mod"/>s.</returns>
public virtual IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods) => Array.Empty<Mod>();
/// <summary>
/// Converts mods to legacy enum values. Do not override if you're not a legacy ruleset.
/// </summary>
/// <param name="mods">The mods which will be converted.</param>
/// <returns>A single bitwise enumerable value representing (to the best of our ability) the mods.</returns>
public virtual LegacyMods ConvertToLegacyMods(Mod[] mods)
{
var value = LegacyMods.None;
foreach (var mod in mods)
{
switch (mod)
{
case ModNoFail _:
value |= LegacyMods.NoFail;
break;
case ModEasy _:
value |= LegacyMods.Easy;
break;
case ModHidden _:
value |= LegacyMods.Hidden;
break;
case ModHardRock _:
value |= LegacyMods.HardRock;
break;
case ModSuddenDeath _:
value |= LegacyMods.SuddenDeath;
break;
case ModDoubleTime _:
value |= LegacyMods.DoubleTime;
break;
case ModRelax _:
value |= LegacyMods.Relax;
break;
case ModHalfTime _:
value |= LegacyMods.HalfTime;
break;
case ModFlashlight _:
value |= LegacyMods.Flashlight;
break;
}
}
return value;
}
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().First();

View File

@ -262,6 +262,21 @@ namespace osu.Game.Rulesets.UI
Playfield.Add(drawableObject);
}
public override void SetRecordTarget(Replay recordingReplay)
{
if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager))
throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available");
var recorder = CreateReplayRecorder(recordingReplay);
if (recorder == null)
return;
recorder.ScreenSpaceToGamefield = Playfield.ScreenSpaceToGamefield;
recordingInputManager.Recorder = recorder;
}
public override void SetReplayScore(Score replayScore)
{
if (!(KeyBindingInputManager is IHasReplayHandler replayInputManager))
@ -472,6 +487,12 @@ namespace osu.Game.Rulesets.UI
/// <param name="replayScore">The replay, null for local input.</param>
public abstract void SetReplayScore(Score replayScore);
/// <summary>
/// Sets a replay to be used to record gameplay.
/// </summary>
/// <param name="recordingReplay">The target to be recorded to.</param>
public abstract void SetRecordTarget(Replay recordingReplay);
/// <summary>
/// Invoked when the interactive user requests resuming from a paused state.
/// Allows potentially delaying the resume process until an interaction is performed.

View File

@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public Func<Vector2, Vector2> GamefieldToScreenSpace => HitObjectContainer.ToScreenSpace;
/// <summary>
/// A function that converts screen space coordinates to gamefield.
/// </summary>
public Func<Vector2, Vector2> ScreenSpaceToGamefield => HitObjectContainer.ToLocalSpace;
/// <summary>
/// All the <see cref="DrawableHitObject"/>s contained in this <see cref="Playfield"/> and all <see cref="NestedPlayfields"/>.
/// </summary>

View File

@ -1,15 +1,16 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.UI
{
@ -66,16 +67,19 @@ namespace osu.Game.Rulesets.UI
if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate))
return;
var frame = HandleFrame(inputManager.CurrentState, pressedActions, last);
var position = ScreenSpaceToGamefield?.Invoke(inputManager.CurrentState.Mouse.Position) ?? inputManager.CurrentState.Mouse.Position;
var frame = HandleFrame(position, pressedActions, last);
if (frame != null)
target.Frames.Add(frame);
}
protected abstract ReplayFrame HandleFrame(InputState state, List<T> testActions, ReplayFrame previousFrame);
protected abstract ReplayFrame HandleFrame(Vector2 mousePosition, List<T> actions, ReplayFrame previousFrame);
}
public abstract class ReplayRecorder : Component
{
public Func<Vector2, Vector2> ScreenSpaceToGamefield;
}
}

View File

@ -24,7 +24,7 @@ using MouseState = osu.Framework.Input.States.MouseState;
namespace osu.Game.Rulesets.UI
{
public abstract class RulesetInputManager<T> : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler
public abstract class RulesetInputManager<T> : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler, IHasRecordingHandler
where T : struct
{
private ReplayRecorder recorder;
@ -184,6 +184,11 @@ namespace osu.Game.Rulesets.UI
ReplayInputHandler ReplayInputHandler { get; set; }
}
public interface IHasRecordingHandler
{
public ReplayRecorder Recorder { set; }
}
/// <summary>
/// Supports attaching a <see cref="KeyCounterDisplay"/>.
/// Keys will be populated automatically and a receptor will be injected inside.

View File

@ -7,15 +7,15 @@ using osu.Game.Rulesets;
namespace osu.Game.Scoring.Legacy
{
/// <summary>
/// A <see cref="LegacyScoreParser"/> which retrieves the applicable <see cref="Beatmap"/> and <see cref="Ruleset"/>
/// A <see cref="LegacyScoreDecoder"/> which retrieves the applicable <see cref="Beatmap"/> and <see cref="Ruleset"/>
/// for the score from the database.
/// </summary>
public class DatabasedLegacyScoreParser : LegacyScoreParser
public class DatabasedLegacyScoreDecoder : LegacyScoreDecoder
{
private readonly RulesetStore rulesets;
private readonly BeatmapManager beatmaps;
public DatabasedLegacyScoreParser(RulesetStore rulesets, BeatmapManager beatmaps)
public DatabasedLegacyScoreDecoder(RulesetStore rulesets, BeatmapManager beatmaps)
{
this.rulesets = rulesets;
this.beatmaps = beatmaps;

View File

@ -19,7 +19,7 @@ using SharpCompress.Compressors.LZMA;
namespace osu.Game.Scoring.Legacy
{
public abstract class LegacyScoreParser
public abstract class LegacyScoreDecoder
{
private IBeatmap currentBeatmap;
private Ruleset currentRuleset;
@ -66,7 +66,7 @@ namespace osu.Game.Scoring.Legacy
/* score.Perfect = */
sr.ReadBoolean();
scoreInfo.Mods = currentRuleset.ConvertLegacyMods((LegacyMods)sr.ReadInt32()).ToArray();
scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray();
/* score.HpGraphString = */
sr.ReadString();
@ -264,7 +264,7 @@ namespace osu.Game.Scoring.Legacy
if (convertible == null)
throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}");
convertible.ConvertFrom(currentFrame, currentBeatmap, lastFrame);
convertible.FromLegacy(currentFrame, currentBeatmap, lastFrame);
var frame = (ReplayFrame)convertible;
frame.Time = currentFrame.Time;

View File

@ -0,0 +1,114 @@
// 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 System.IO;
using System.Linq;
using System.Text;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.IO.Legacy;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays.Types;
using SharpCompress.Compressors.LZMA;
namespace osu.Game.Scoring.Legacy
{
public class LegacyScoreEncoder
{
public const int LATEST_VERSION = 128;
private readonly Score score;
private readonly IBeatmap beatmap;
public LegacyScoreEncoder(Score score, IBeatmap beatmap)
{
this.score = score;
this.beatmap = beatmap;
if (score.ScoreInfo.Beatmap.RulesetID < 0 || score.ScoreInfo.Beatmap.RulesetID > 3)
throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score));
}
public void Encode(Stream stream)
{
using (SerializationWriter sw = new SerializationWriter(stream))
{
sw.Write((byte)(score.ScoreInfo.Ruleset.ID ?? 0));
sw.Write(LATEST_VERSION);
sw.Write(score.ScoreInfo.Beatmap.MD5Hash);
sw.Write(score.ScoreInfo.UserString);
sw.Write($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}".ComputeMD5Hash());
sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0));
sw.Write((ushort)(score.ScoreInfo.GetCount100() ?? 0));
sw.Write((ushort)(score.ScoreInfo.GetCount50() ?? 0));
sw.Write((ushort)(score.ScoreInfo.GetCountGeki() ?? 0));
sw.Write((ushort)(score.ScoreInfo.GetCountKatu() ?? 0));
sw.Write((ushort)(score.ScoreInfo.GetCountMiss() ?? 0));
sw.Write((int)(score.ScoreInfo.TotalScore));
sw.Write((ushort)score.ScoreInfo.MaxCombo);
sw.Write(score.ScoreInfo.Combo == score.ScoreInfo.MaxCombo);
sw.Write((int)score.ScoreInfo.Ruleset.CreateInstance().ConvertToLegacyMods(score.ScoreInfo.Mods));
sw.Write(getHpGraphFormatted());
sw.Write(score.ScoreInfo.Date.DateTime);
sw.WriteByteArray(createReplayData());
sw.Write((long)0);
writeModSpecificData(score.ScoreInfo, sw);
}
}
private void writeModSpecificData(ScoreInfo score, SerializationWriter sw)
{
}
private byte[] createReplayData()
{
var content = new ASCIIEncoding().GetBytes(replayStringContent);
using (var outStream = new MemoryStream())
{
using (var lzma = new LzmaStream(new LzmaEncoderProperties(false, 1 << 21, 255), false, outStream))
{
outStream.Write(lzma.Properties);
long fileSize = content.Length;
for (int i = 0; i < 8; i++)
outStream.WriteByte((byte)(fileSize >> (8 * i)));
lzma.Write(content);
}
return outStream.ToArray();
}
}
private string replayStringContent
{
get
{
StringBuilder replayData = new StringBuilder();
if (score.Replay != null)
{
LegacyReplayFrame lastF = new LegacyReplayFrame(0, 0, 0, ReplayButtonState.None);
foreach (var f in score.Replay.Frames.OfType<IConvertibleReplayFrame>().Select(f => f.ToLegacy(beatmap)))
{
replayData.Append(FormattableString.Invariant($"{f.Time - lastF.Time}|{f.MouseX ?? 0}|{f.MouseY ?? 0}|{(int)f.ButtonState},"));
lastF = f;
}
}
replayData.AppendFormat(@"{0}|{1}|{2}|{3},", -12345, 0, 0, 0);
return replayData.ToString();
}
}
private string getHpGraphFormatted()
{
// todo: implement, maybe?
return string.Empty;
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Scoring
var replayFilename = score.Files.First(f => f.Filename.EndsWith(".osr", StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath;
using (var stream = store.GetStream(replayFilename))
Replay = new DatabasedLegacyScoreParser(rulesets, beatmaps).Parse(stream).Replay;
Replay = new DatabasedLegacyScoreDecoder(rulesets, beatmaps).Parse(stream).Replay;
}
}
}

View File

@ -46,9 +46,9 @@ namespace osu.Game.Scoring
{
try
{
return new DatabasedLegacyScoreParser(rulesets, beatmaps()).Parse(stream).ScoreInfo;
return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo;
}
catch (LegacyScoreParser.BeatmapNotFoundException e)
catch (LegacyScoreDecoder.BeatmapNotFoundException e)
{
Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error);
return null;

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -17,13 +18,16 @@ using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osu.Game.Users;
@ -117,6 +121,23 @@ namespace osu.Game.Screens.Play
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
protected override void LoadComplete()
{
base.LoadComplete();
PrepareReplay();
}
private Replay recordingReplay;
/// <summary>
/// Run any recording / playback setup for replays.
/// </summary>
protected virtual void PrepareReplay()
{
DrawableRuleset.SetRecordTarget(recordingReplay = new Replay());
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuConfigManager config)
{
@ -622,19 +643,33 @@ namespace osu.Game.Screens.Play
completionProgressDelegate?.Cancel();
completionProgressDelegate = Schedule(delegate
{
var score = CreateScore();
if (DrawableRuleset.ReplayScore == null)
{
scoreManager.Import(score).ContinueWith(_ => Schedule(() =>
{
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(score));
}));
}
if (DrawableRuleset.ReplayScore != null)
this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo));
else
this.Push(CreateResults(score));
{
var score = new Score { ScoreInfo = CreateScore() };
LegacyByteArrayReader replayReader = null;
if (recordingReplay?.Frames.Count > 0)
{
score.Replay = recordingReplay;
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, gameplayBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
}
scoreManager.Import(score.ScoreInfo, replayReader)
.ContinueWith(imported => Schedule(() =>
{
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(imported.Result));
}));
}
});
}

View File

@ -18,9 +18,8 @@ namespace osu.Game.Screens.Play
this.score = score;
}
protected override void LoadComplete()
protected override void PrepareReplay()
{
base.LoadComplete();
DrawableRuleset?.SetReplayScore(score);
}

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Beatmaps
protected void Test(LegacyMods legacyMods, Type[] expectedMods)
{
var ruleset = CreateRuleset();
var mods = ruleset.ConvertLegacyMods(legacyMods).ToList();
var mods = ruleset.ConvertFromLegacyMods(legacyMods).ToList();
Assert.AreEqual(expectedMods.Length, mods.Count);
foreach (var modType in expectedMods)