mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 15:44:04 +09:00
Merge branch 'master' into storyboard-rewind-support
This commit is contained in:
@ -8,6 +8,7 @@ using OpenTK.Graphics;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Framework;
|
||||
|
||||
namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
@ -21,6 +22,19 @@ namespace osu.Game.Beatmaps.Formats
|
||||
private LegacySampleBank defaultSampleBank;
|
||||
private int defaultSampleVolume = 100;
|
||||
|
||||
/// <summary>
|
||||
/// lazer's audio timings in general doesn't match stable. this is the result of user testing, albeit limited.
|
||||
/// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
||||
/// </summary>
|
||||
public static int UniversalOffset => RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? -22 : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not beatmap or runtime offsets should be applied. Defaults on; only disable for testing purposes.
|
||||
/// </summary>
|
||||
public bool ApplyOffsets = true;
|
||||
|
||||
private readonly int offset = UniversalOffset;
|
||||
|
||||
public LegacyBeatmapDecoder()
|
||||
{
|
||||
}
|
||||
@ -28,6 +42,9 @@ namespace osu.Game.Beatmaps.Formats
|
||||
public LegacyBeatmapDecoder(string header)
|
||||
{
|
||||
BeatmapVersion = int.Parse(header.Substring(17));
|
||||
|
||||
// BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off)
|
||||
offset += BeatmapVersion < 5 ? 24 : 0;
|
||||
}
|
||||
|
||||
protected override void ParseBeatmap(StreamReader stream, Beatmap beatmap)
|
||||
@ -102,7 +119,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value);
|
||||
break;
|
||||
case @"PreviewTime":
|
||||
metadata.PreviewTime = int.Parse(pair.Value);
|
||||
metadata.PreviewTime = getOffsetTime(int.Parse(pair.Value));
|
||||
break;
|
||||
case @"Countdown":
|
||||
beatmap.BeatmapInfo.Countdown = int.Parse(pair.Value) == 1;
|
||||
@ -257,8 +274,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
case EventType.Break:
|
||||
var breakEvent = new BreakPeriod
|
||||
{
|
||||
StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo),
|
||||
EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo)
|
||||
StartTime = getOffsetTime(double.Parse(split[1], NumberFormatInfo.InvariantInfo)),
|
||||
EndTime = getOffsetTime(double.Parse(split[2], NumberFormatInfo.InvariantInfo))
|
||||
};
|
||||
|
||||
if (!breakEvent.HasEffect)
|
||||
@ -273,7 +290,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
string[] split = line.Split(',');
|
||||
|
||||
double time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo);
|
||||
double time = getOffsetTime(double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo));
|
||||
double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo);
|
||||
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
|
||||
|
||||
@ -396,7 +413,14 @@ namespace osu.Game.Beatmaps.Formats
|
||||
var obj = parser.Parse(line);
|
||||
|
||||
if (obj != null)
|
||||
{
|
||||
obj.StartTime = getOffsetTime(obj.StartTime);
|
||||
beatmap.HitObjects.Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);
|
||||
|
||||
private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0);
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
Vector2 offset = (input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.NativeState.Position) - DrawSize / 2) * ParallaxAmount;
|
||||
|
||||
content.Position = Interpolation.ValueAt(Clock.ElapsedFrameTime, content.Position, offset, 0, 1000, Easing.OutQuint);
|
||||
content.Position = Interpolation.ValueAt(MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 1000), content.Position, offset, 0, 1000, Easing.OutQuint);
|
||||
content.Scale = new Vector2(1 + ParallaxAmount);
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Game.Input.Handlers;
|
||||
using OpenTK;
|
||||
using OpenTK.Input;
|
||||
@ -17,14 +16,15 @@ namespace osu.Game.Rulesets.Replays
|
||||
/// The ReplayHandler will take a replay and handle the propagation of updates to the input stack.
|
||||
/// It handles logic of any frames which *must* be executed.
|
||||
/// </summary>
|
||||
public abstract class FramedReplayInputHandler : ReplayInputHandler
|
||||
public abstract class FramedReplayInputHandler<TFrame> : ReplayInputHandler
|
||||
where TFrame : ReplayFrame
|
||||
{
|
||||
private readonly Replay replay;
|
||||
|
||||
protected List<ReplayFrame> Frames => replay.Frames;
|
||||
|
||||
public ReplayFrame CurrentFrame => !hasFrames ? null : Frames[currentFrameIndex];
|
||||
public ReplayFrame NextFrame => !hasFrames ? null : Frames[nextFrameIndex];
|
||||
public TFrame CurrentFrame => !HasFrames ? null : (TFrame)Frames[currentFrameIndex];
|
||||
public TFrame NextFrame => !HasFrames ? null : (TFrame)Frames[nextFrameIndex];
|
||||
|
||||
private int currentFrameIndex;
|
||||
|
||||
@ -46,31 +46,14 @@ namespace osu.Game.Rulesets.Replays
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetPosition(Vector2 pos)
|
||||
{
|
||||
}
|
||||
|
||||
protected Vector2? Position
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!hasFrames)
|
||||
return null;
|
||||
|
||||
return Interpolation.ValueAt(currentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time);
|
||||
}
|
||||
}
|
||||
|
||||
public override List<InputState> GetPendingStates() => new List<InputState>();
|
||||
|
||||
public bool AtLastFrame => currentFrameIndex == Frames.Count - 1;
|
||||
public bool AtFirstFrame => currentFrameIndex == 0;
|
||||
|
||||
public Vector2 Size => new Vector2(512, 384);
|
||||
|
||||
private const double sixty_frame_time = 1000.0 / 60;
|
||||
|
||||
private double currentTime;
|
||||
protected double CurrentTime { get; private set; }
|
||||
private int currentDirection;
|
||||
|
||||
/// <summary>
|
||||
@ -79,14 +62,16 @@ namespace osu.Game.Rulesets.Replays
|
||||
/// </summary>
|
||||
public bool FrameAccuratePlayback = true;
|
||||
|
||||
private bool hasFrames => Frames.Count > 0;
|
||||
protected bool HasFrames => Frames.Count > 0;
|
||||
|
||||
private bool inImportantSection =>
|
||||
FrameAccuratePlayback &&
|
||||
HasFrames && FrameAccuratePlayback &&
|
||||
//a button is in a pressed state
|
||||
((currentDirection > 0 ? CurrentFrame : NextFrame)?.IsImportant ?? false) &&
|
||||
IsImportant(currentDirection > 0 ? CurrentFrame : NextFrame) &&
|
||||
//the next frame is within an allowable time span
|
||||
Math.Abs(currentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2;
|
||||
Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2;
|
||||
|
||||
protected virtual bool IsImportant(TFrame frame) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Update the current frame based on an incoming time value.
|
||||
@ -97,10 +82,10 @@ namespace osu.Game.Rulesets.Replays
|
||||
/// <returns>The usable time value. If null, we should not advance time as we do not have enough data.</returns>
|
||||
public override double? SetFrameFromTime(double time)
|
||||
{
|
||||
currentDirection = time.CompareTo(currentTime);
|
||||
currentDirection = time.CompareTo(CurrentTime);
|
||||
if (currentDirection == 0) currentDirection = 1;
|
||||
|
||||
if (hasFrames)
|
||||
if (HasFrames)
|
||||
{
|
||||
// check if the next frame is in the "future" for the current playback direction
|
||||
if (currentDirection != time.CompareTo(NextFrame.Time))
|
||||
@ -114,12 +99,12 @@ namespace osu.Game.Rulesets.Replays
|
||||
// If going backwards, we need to execute once _before_ the frame time to reverse any judgements
|
||||
// that would occur as a result of this frame in forward playback
|
||||
if (currentDirection == -1)
|
||||
return currentTime = CurrentFrame.Time - 1;
|
||||
return currentTime = CurrentFrame.Time;
|
||||
return CurrentTime = CurrentFrame.Time - 1;
|
||||
return CurrentTime = CurrentFrame.Time;
|
||||
}
|
||||
}
|
||||
|
||||
return currentTime = time;
|
||||
return CurrentTime = time;
|
||||
}
|
||||
|
||||
protected class ReplayMouseState : MouseState
|
||||
|
38
osu.Game/Rulesets/Replays/Legacy/LegacyReplayFrame.cs
Normal file
38
osu.Game/Rulesets/Replays/Legacy/LegacyReplayFrame.cs
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Replays.Legacy
|
||||
{
|
||||
public class LegacyReplayFrame : ReplayFrame
|
||||
{
|
||||
public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0);
|
||||
|
||||
public float? MouseX;
|
||||
public float? MouseY;
|
||||
|
||||
public bool MouseLeft => MouseLeft1 || MouseLeft2;
|
||||
public bool MouseRight => MouseRight1 || MouseRight2;
|
||||
|
||||
public bool MouseLeft1 => (ButtonState & ReplayButtonState.Left1) > 0;
|
||||
public bool MouseRight1 => (ButtonState & ReplayButtonState.Right1) > 0;
|
||||
public bool MouseLeft2 => (ButtonState & ReplayButtonState.Left2) > 0;
|
||||
public bool MouseRight2 => (ButtonState & ReplayButtonState.Right2) > 0;
|
||||
|
||||
public ReplayButtonState ButtonState;
|
||||
|
||||
public LegacyReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState)
|
||||
: base(time)
|
||||
{
|
||||
MouseX = mouseX;
|
||||
MouseY = mouseY;
|
||||
ButtonState = buttonState;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Rulesets.Replays
|
||||
namespace osu.Game.Rulesets.Replays.Legacy
|
||||
{
|
||||
[Flags]
|
||||
public enum ReplayButtonState
|
@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Replays
|
||||
public class Replay
|
||||
{
|
||||
public User User;
|
||||
|
||||
public List<ReplayFrame> Frames = new List<ReplayFrame>();
|
||||
}
|
||||
}
|
||||
|
@ -1,70 +1,19 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Replays
|
||||
{
|
||||
public class ReplayFrame
|
||||
{
|
||||
public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0);
|
||||
|
||||
public virtual bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight);
|
||||
|
||||
public float? MouseX;
|
||||
public float? MouseY;
|
||||
|
||||
public bool MouseLeft => MouseLeft1 || MouseLeft2;
|
||||
public bool MouseRight => MouseRight1 || MouseRight2;
|
||||
|
||||
public bool MouseLeft1
|
||||
{
|
||||
get { return (ButtonState & ReplayButtonState.Left1) > 0; }
|
||||
set { setButtonState(ReplayButtonState.Left1, value); }
|
||||
}
|
||||
public bool MouseRight1
|
||||
{
|
||||
get { return (ButtonState & ReplayButtonState.Right1) > 0; }
|
||||
set { setButtonState(ReplayButtonState.Right1, value); }
|
||||
}
|
||||
public bool MouseLeft2
|
||||
{
|
||||
get { return (ButtonState & ReplayButtonState.Left2) > 0; }
|
||||
set { setButtonState(ReplayButtonState.Left2, value); }
|
||||
}
|
||||
public bool MouseRight2
|
||||
{
|
||||
get { return (ButtonState & ReplayButtonState.Right2) > 0; }
|
||||
set { setButtonState(ReplayButtonState.Right2, value); }
|
||||
}
|
||||
|
||||
private void setButtonState(ReplayButtonState singleButton, bool pressed)
|
||||
{
|
||||
if (pressed)
|
||||
ButtonState |= singleButton;
|
||||
else
|
||||
ButtonState &= ~singleButton;
|
||||
}
|
||||
|
||||
public double Time;
|
||||
|
||||
public ReplayButtonState ButtonState;
|
||||
|
||||
protected ReplayFrame()
|
||||
public ReplayFrame()
|
||||
{
|
||||
}
|
||||
|
||||
public ReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState)
|
||||
public ReplayFrame(double time)
|
||||
{
|
||||
MouseX = mouseX;
|
||||
MouseY = mouseY;
|
||||
ButtonState = buttonState;
|
||||
Time = time;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs
Normal file
22
osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Replays.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Replays.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of <see cref="ReplayFrame"/> which can be converted from a <see cref="LegacyReplayFrame"/>.
|
||||
/// </summary>
|
||||
public interface IConvertibleReplayFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// Populates this <see cref="ReplayFrame"/> using values from a <see cref="LegacyReplayFrame"/>.
|
||||
/// </summary>
|
||||
/// <param name="legacyFrame">The <see cref="LegacyReplayFrame"/> to extract values from.</param>
|
||||
/// <param name="score">The score.</param>
|
||||
/// <param name="beatmap">The beatmap.</param>
|
||||
void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap);
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
@ -89,6 +90,13 @@ namespace osu.Game.Rulesets
|
||||
/// <returns>A descriptive name of the variant.</returns>
|
||||
public virtual string GetVariantName(int variant) => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// For rulesets which support legacy (osu-stable) replay conversion, this method will create an empty replay frame
|
||||
/// for conversion use.
|
||||
/// </summary>
|
||||
/// <returns>An empty frame for the current ruleset, or null if unsupported.</returns>
|
||||
public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Create a ruleset info based on this ruleset.
|
||||
/// </summary>
|
||||
|
152
osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs
Normal file
152
osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs
Normal file
@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.IO.Legacy;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Replays.Legacy;
|
||||
using osu.Game.Users;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
{
|
||||
public class LegacyScoreParser
|
||||
{
|
||||
private readonly RulesetStore rulesets;
|
||||
private readonly BeatmapManager beatmaps;
|
||||
|
||||
public LegacyScoreParser(RulesetStore rulesets, BeatmapManager beatmaps)
|
||||
{
|
||||
this.rulesets = rulesets;
|
||||
this.beatmaps = beatmaps;
|
||||
}
|
||||
|
||||
private Beatmap currentBeatmap;
|
||||
private Ruleset currentRuleset;
|
||||
|
||||
public Score Parse(Stream stream)
|
||||
{
|
||||
Score score;
|
||||
|
||||
using (SerializationReader sr = new SerializationReader(stream))
|
||||
{
|
||||
score = new Score { Ruleset = rulesets.GetRuleset(sr.ReadByte()) };
|
||||
currentRuleset = score.Ruleset.CreateInstance();
|
||||
|
||||
/* score.Pass = true;*/
|
||||
var version = sr.ReadInt32();
|
||||
|
||||
/* score.FileChecksum = */
|
||||
var beatmapHash = sr.ReadString();
|
||||
score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash);
|
||||
currentBeatmap = beatmaps.GetWorkingBeatmap(score.Beatmap).Beatmap;
|
||||
|
||||
/* score.PlayerName = */
|
||||
score.User = new User { Username = sr.ReadString() };
|
||||
/* var localScoreChecksum = */
|
||||
sr.ReadString();
|
||||
/* score.Count300 = */
|
||||
sr.ReadUInt16();
|
||||
/* score.Count100 = */
|
||||
sr.ReadUInt16();
|
||||
/* score.Count50 = */
|
||||
sr.ReadUInt16();
|
||||
/* score.CountGeki = */
|
||||
sr.ReadUInt16();
|
||||
/* score.CountKatu = */
|
||||
sr.ReadUInt16();
|
||||
/* score.CountMiss = */
|
||||
sr.ReadUInt16();
|
||||
score.TotalScore = sr.ReadInt32();
|
||||
score.MaxCombo = sr.ReadUInt16();
|
||||
/* score.Perfect = */
|
||||
sr.ReadBoolean();
|
||||
/* score.EnabledMods = (Mods)*/
|
||||
sr.ReadInt32();
|
||||
/* score.HpGraphString = */
|
||||
sr.ReadString();
|
||||
/* score.Date = */
|
||||
sr.ReadDateTime();
|
||||
|
||||
var compressedReplay = sr.ReadByteArray();
|
||||
|
||||
if (version >= 20140721)
|
||||
/*OnlineId =*/
|
||||
sr.ReadInt64();
|
||||
else if (version >= 20121008)
|
||||
/*OnlineId =*/
|
||||
sr.ReadInt32();
|
||||
|
||||
using (var replayInStream = new MemoryStream(compressedReplay))
|
||||
{
|
||||
byte[] properties = new byte[5];
|
||||
if (replayInStream.Read(properties, 0, 5) != 5)
|
||||
throw new IOException("input .lzma is too short");
|
||||
long outSize = 0;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
int v = replayInStream.ReadByte();
|
||||
if (v < 0)
|
||||
throw new IOException("Can't Read 1");
|
||||
outSize |= (long)(byte)v << (8 * i);
|
||||
}
|
||||
|
||||
long compressedSize = replayInStream.Length - replayInStream.Position;
|
||||
|
||||
using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize))
|
||||
using (var reader = new StreamReader(lzma))
|
||||
{
|
||||
score.Replay = new Replay { User = score.User };
|
||||
readLegacyReplay(score.Replay, reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private void readLegacyReplay(Replay replay, StreamReader reader)
|
||||
{
|
||||
float lastTime = 0;
|
||||
|
||||
foreach (var l in reader.ReadToEnd().Split(','))
|
||||
{
|
||||
var split = l.Split('|');
|
||||
|
||||
if (split.Length < 4)
|
||||
continue;
|
||||
|
||||
if (split[0] == "-12345")
|
||||
{
|
||||
// Todo: The seed is provided in split[3], which we'll need to use at some point
|
||||
continue;
|
||||
}
|
||||
|
||||
var diff = float.Parse(split[0]);
|
||||
lastTime += diff;
|
||||
|
||||
// Todo: At some point we probably want to rewind and play back the negative-time frames
|
||||
// but for now we'll achieve equal playback to stable by skipping negative frames
|
||||
if (diff < 0)
|
||||
continue;
|
||||
|
||||
replay.Frames.Add(convertFrame(new LegacyReplayFrame(lastTime, float.Parse(split[1]), float.Parse(split[2]), (ReplayButtonState)int.Parse(split[3]))));
|
||||
}
|
||||
}
|
||||
|
||||
private ReplayFrame convertFrame(LegacyReplayFrame legacyFrame)
|
||||
{
|
||||
var convertible = currentRuleset.CreateConvertibleReplayFrame();
|
||||
if (convertible == null)
|
||||
throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}");
|
||||
convertible.ConvertFrom(legacyFrame, currentBeatmap);
|
||||
|
||||
var frame = (ReplayFrame)convertible;
|
||||
frame.Time = legacyFrame.Time;
|
||||
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,16 +2,12 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO.Legacy;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Users;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
@ -53,127 +49,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
public Score ReadReplayFile(string replayFilename)
|
||||
{
|
||||
Score score;
|
||||
|
||||
using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename)))
|
||||
using (SerializationReader sr = new SerializationReader(s))
|
||||
{
|
||||
score = new Score
|
||||
{
|
||||
Ruleset = rulesets.GetRuleset(sr.ReadByte())
|
||||
};
|
||||
|
||||
/* score.Pass = true;*/
|
||||
var version = sr.ReadInt32();
|
||||
/* score.FileChecksum = */
|
||||
var beatmapHash = sr.ReadString();
|
||||
score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash);
|
||||
/* score.PlayerName = */
|
||||
score.User = new User { Username = sr.ReadString() };
|
||||
/* var localScoreChecksum = */
|
||||
sr.ReadString();
|
||||
/* score.Count300 = */
|
||||
sr.ReadUInt16();
|
||||
/* score.Count100 = */
|
||||
sr.ReadUInt16();
|
||||
/* score.Count50 = */
|
||||
sr.ReadUInt16();
|
||||
/* score.CountGeki = */
|
||||
sr.ReadUInt16();
|
||||
/* score.CountKatu = */
|
||||
sr.ReadUInt16();
|
||||
/* score.CountMiss = */
|
||||
sr.ReadUInt16();
|
||||
score.TotalScore = sr.ReadInt32();
|
||||
score.MaxCombo = sr.ReadUInt16();
|
||||
/* score.Perfect = */
|
||||
sr.ReadBoolean();
|
||||
/* score.EnabledMods = (Mods)*/
|
||||
sr.ReadInt32();
|
||||
/* score.HpGraphString = */
|
||||
sr.ReadString();
|
||||
/* score.Date = */
|
||||
sr.ReadDateTime();
|
||||
|
||||
var compressedReplay = sr.ReadByteArray();
|
||||
|
||||
if (version >= 20140721)
|
||||
/*OnlineId =*/
|
||||
sr.ReadInt64();
|
||||
else if (version >= 20121008)
|
||||
/*OnlineId =*/
|
||||
sr.ReadInt32();
|
||||
|
||||
using (var replayInStream = new MemoryStream(compressedReplay))
|
||||
{
|
||||
byte[] properties = new byte[5];
|
||||
if (replayInStream.Read(properties, 0, 5) != 5)
|
||||
throw new IOException("input .lzma is too short");
|
||||
long outSize = 0;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
int v = replayInStream.ReadByte();
|
||||
if (v < 0)
|
||||
throw new IOException("Can't Read 1");
|
||||
outSize |= (long)(byte)v << (8 * i);
|
||||
}
|
||||
|
||||
long compressedSize = replayInStream.Length - replayInStream.Position;
|
||||
|
||||
using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize))
|
||||
using (var reader = new StreamReader(lzma))
|
||||
{
|
||||
score.Replay = createLegacyReplay(reader);
|
||||
score.Replay.User = score.User;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
return new LegacyScoreParser(rulesets, beatmaps).Parse(s);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a legacy replay which is read from a stream.
|
||||
/// </summary>
|
||||
/// <param name="reader">The stream reader.</param>
|
||||
/// <returns>The legacy replay.</returns>
|
||||
private Replay createLegacyReplay(StreamReader reader)
|
||||
{
|
||||
var frames = new List<ReplayFrame>();
|
||||
|
||||
float lastTime = 0;
|
||||
|
||||
foreach (var l in reader.ReadToEnd().Split(','))
|
||||
{
|
||||
var split = l.Split('|');
|
||||
|
||||
if (split.Length < 4)
|
||||
continue;
|
||||
|
||||
if (split[0] == "-12345")
|
||||
{
|
||||
// Todo: The seed is provided in split[3], which we'll need to use at some point
|
||||
continue;
|
||||
}
|
||||
|
||||
var diff = float.Parse(split[0]);
|
||||
lastTime += diff;
|
||||
|
||||
// Todo: At some point we probably want to rewind and play back the negative-time frames
|
||||
// but for now we'll achieve equal playback to stable by skipping negative frames
|
||||
if (diff < 0)
|
||||
continue;
|
||||
|
||||
frames.Add(new ReplayFrame(
|
||||
lastTime,
|
||||
float.Parse(split[1]),
|
||||
float.Parse(split[2]),
|
||||
(ReplayButtonState)int.Parse(split[3])
|
||||
));
|
||||
}
|
||||
|
||||
return new Replay { Frames = frames };
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <returns>The input manager.</returns>
|
||||
public abstract PassThroughInputManager CreateInputManager();
|
||||
|
||||
protected virtual FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => null;
|
||||
protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null;
|
||||
|
||||
public Replay Replay { get; private set; }
|
||||
|
||||
|
@ -91,8 +91,6 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
#region Clock control
|
||||
|
||||
protected override bool ShouldProcessClock => false; // We handle processing the clock ourselves
|
||||
|
||||
private ManualClock clock;
|
||||
private IFrameBasedClock parentClock;
|
||||
|
||||
@ -103,6 +101,7 @@ namespace osu.Game.Rulesets.UI
|
||||
//our clock will now be our parent's clock, but we want to replace this to allow manual control.
|
||||
parentClock = Clock;
|
||||
|
||||
ProcessCustomClock = false;
|
||||
Clock = new FramedClock(clock = new ManualClock
|
||||
{
|
||||
CurrentTime = parentClock.CurrentTime,
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private static bool hasShownNotificationOnce;
|
||||
|
||||
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, DecoupleableInterpolatingFramedClock decoupledClock, WorkingBeatmap working, IAdjustableClock adjustableSourceClock)
|
||||
public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IClock offsetClock, IAdjustableClock adjustableClock)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@ -66,13 +66,13 @@ namespace osu.Game.Screens.Play
|
||||
BindRulesetContainer(rulesetContainer);
|
||||
|
||||
Progress.Objects = rulesetContainer.Objects;
|
||||
Progress.AudioClock = decoupledClock;
|
||||
Progress.AudioClock = offsetClock;
|
||||
Progress.AllowSeeking = rulesetContainer.HasReplayLoaded;
|
||||
Progress.OnSeek = pos => decoupledClock.Seek(pos);
|
||||
Progress.OnSeek = pos => adjustableClock.Seek(pos);
|
||||
|
||||
ModDisplay.Current.BindTo(working.Mods);
|
||||
|
||||
PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableSourceClock;
|
||||
PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
|
@ -44,14 +44,22 @@ namespace osu.Game.Screens.Play
|
||||
public Action OnResume;
|
||||
public Action OnPause;
|
||||
|
||||
public IAdjustableClock AudioClock;
|
||||
public FramedClock FramedClock;
|
||||
private readonly IAdjustableClock adjustableClock;
|
||||
private readonly FramedClock framedClock;
|
||||
|
||||
public PauseContainer()
|
||||
public PauseContainer(FramedClock framedClock, IAdjustableClock adjustableClock)
|
||||
{
|
||||
this.framedClock = framedClock;
|
||||
this.adjustableClock = adjustableClock;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
AddInternal(content = new Container { RelativeSizeAxes = Axes.Both });
|
||||
AddInternal(content = new Container
|
||||
{
|
||||
Clock = this.framedClock,
|
||||
ProcessCustomClock = false,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
|
||||
AddInternal(pauseOverlay = new PauseOverlay
|
||||
{
|
||||
@ -65,47 +73,37 @@ namespace osu.Game.Screens.Play
|
||||
});
|
||||
}
|
||||
|
||||
public void Pause(bool force = false)
|
||||
public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called.
|
||||
{
|
||||
if (!CanPause && !force) return;
|
||||
|
||||
if (IsPaused) return;
|
||||
|
||||
// stop the decoupled clock (stops the audio eventually)
|
||||
AudioClock.Stop();
|
||||
|
||||
// stop processing updatess on the offset clock (instantly freezes time for all our components)
|
||||
FramedClock.ProcessSourceClockFrames = false;
|
||||
|
||||
// stop the seekable clock (stops the audio eventually)
|
||||
adjustableClock.Stop();
|
||||
IsPaused = true;
|
||||
|
||||
// we need to do a final check after all of our children have processed up to the paused clock time.
|
||||
// this is to cover cases where, for instance, the player fails in the current processing frame.
|
||||
Schedule(() =>
|
||||
{
|
||||
if (!CanPause) return;
|
||||
OnPause?.Invoke();
|
||||
pauseOverlay.Show();
|
||||
|
||||
lastPauseActionTime = Time.Current;
|
||||
|
||||
OnPause?.Invoke();
|
||||
pauseOverlay.Show();
|
||||
});
|
||||
}
|
||||
lastPauseActionTime = Time.Current;
|
||||
});
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
if (!IsPaused) return;
|
||||
|
||||
IsPaused = false;
|
||||
FramedClock.ProcessSourceClockFrames = true;
|
||||
|
||||
IsResuming = false;
|
||||
lastPauseActionTime = Time.Current;
|
||||
|
||||
OnResume?.Invoke();
|
||||
// seek back to the time of the framed clock.
|
||||
// this accounts for the audio clock potentially taking time to enter a completely stopped state.
|
||||
adjustableClock.Seek(framedClock.CurrentTime);
|
||||
adjustableClock.Start();
|
||||
|
||||
OnResume?.Invoke();
|
||||
pauseOverlay.Hide();
|
||||
AudioClock.Start();
|
||||
IsResuming = false;
|
||||
}
|
||||
|
||||
private OsuGameBase game;
|
||||
@ -122,6 +120,9 @@ namespace osu.Game.Screens.Play
|
||||
if (!game.IsActive && CanPause)
|
||||
Pause();
|
||||
|
||||
if (!IsPaused)
|
||||
framedClock.ProcessFrame();
|
||||
|
||||
base.Update();
|
||||
}
|
||||
|
||||
|
@ -56,9 +56,12 @@ namespace osu.Game.Screens.Play
|
||||
public CursorContainer Cursor => RulesetContainer.Cursor;
|
||||
public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value;
|
||||
|
||||
private IAdjustableClock adjustableSourceClock;
|
||||
private FramedOffsetClock offsetClock;
|
||||
private DecoupleableInterpolatingFramedClock decoupledClock;
|
||||
private IAdjustableClock sourceClock;
|
||||
|
||||
/// <summary>
|
||||
/// The decoupled clock used for gameplay. Should be used for seeks and clock control.
|
||||
/// </summary>
|
||||
private DecoupleableInterpolatingFramedClock adjustableClock;
|
||||
|
||||
private PauseContainer pauseContainer;
|
||||
|
||||
@ -140,17 +143,18 @@ namespace osu.Game.Screens.Play
|
||||
return;
|
||||
}
|
||||
|
||||
adjustableSourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock();
|
||||
decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
|
||||
sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock();
|
||||
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
|
||||
|
||||
var firstObjectTime = RulesetContainer.Objects.First().StartTime;
|
||||
decoupledClock.Seek(AllowLeadIn
|
||||
adjustableClock.Seek(AllowLeadIn
|
||||
? Math.Min(0, firstObjectTime - Math.Max(beatmap.ControlPointInfo.TimingPointAt(firstObjectTime).BeatLength * 4, beatmap.BeatmapInfo.AudioLeadIn))
|
||||
: firstObjectTime);
|
||||
|
||||
decoupledClock.ProcessFrame();
|
||||
adjustableClock.ProcessFrame();
|
||||
|
||||
offsetClock = new FramedOffsetClock(decoupledClock);
|
||||
// the final usable gameplay clock with user-set offsets applied.
|
||||
var offsetClock = new FramedOffsetClock(adjustableClock);
|
||||
|
||||
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
||||
userAudioOffset.ValueChanged += v => offsetClock.Offset = v;
|
||||
@ -160,16 +164,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
storyboardContainer = new Container
|
||||
pauseContainer = new PauseContainer(offsetClock, adjustableClock)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Clock = offsetClock,
|
||||
Alpha = 0,
|
||||
},
|
||||
pauseContainer = new PauseContainer
|
||||
{
|
||||
AudioClock = decoupledClock,
|
||||
FramedClock = offsetClock,
|
||||
OnRetry = Restart,
|
||||
OnQuit = Exit,
|
||||
CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded,
|
||||
@ -181,15 +177,23 @@ namespace osu.Game.Screens.Play
|
||||
OnResume = () => hudOverlay.KeyCounter.IsCounting = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
storyboardContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Clock = offsetClock,
|
||||
Child = RulesetContainer,
|
||||
Alpha = 0,
|
||||
},
|
||||
new SkipButton(firstObjectTime) { AudioClock = decoupledClock },
|
||||
hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, decoupledClock, working, adjustableSourceClock)
|
||||
RulesetContainer,
|
||||
new SkipButton(firstObjectTime)
|
||||
{
|
||||
Clock = Clock, // skip button doesn't want to use the audio clock directly
|
||||
ProcessCustomClock = false,
|
||||
AdjustableClock = adjustableClock,
|
||||
FramedClock = offsetClock,
|
||||
},
|
||||
hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, working, offsetClock, adjustableClock)
|
||||
{
|
||||
Clock = Clock, // hud overlay doesn't want to use the audio clock directly
|
||||
ProcessCustomClock = false,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
@ -197,7 +201,7 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Clock = decoupledClock,
|
||||
ProcessCustomClock = false,
|
||||
Breaks = beatmap.Breaks
|
||||
}
|
||||
}
|
||||
@ -234,11 +238,11 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private void applyRateFromMods()
|
||||
{
|
||||
if (adjustableSourceClock == null) return;
|
||||
if (sourceClock == null) return;
|
||||
|
||||
adjustableSourceClock.Rate = 1;
|
||||
sourceClock.Rate = 1;
|
||||
foreach (var mod in Beatmap.Value.Mods.Value.OfType<IApplicableToClock>())
|
||||
mod.ApplyToClock(adjustableSourceClock);
|
||||
mod.ApplyToClock(sourceClock);
|
||||
}
|
||||
|
||||
private void initializeStoryboard(bool asyncLoad)
|
||||
@ -297,7 +301,7 @@ namespace osu.Game.Screens.Play
|
||||
if (Beatmap.Value.Mods.Value.OfType<IApplicableFailOverride>().Any(m => !m.AllowFail))
|
||||
return false;
|
||||
|
||||
decoupledClock.Stop();
|
||||
adjustableClock.Stop();
|
||||
|
||||
HasFailed = true;
|
||||
failOverlay.Retries = RestartCount;
|
||||
@ -326,17 +330,19 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
adjustableSourceClock.Reset();
|
||||
sourceClock.Reset();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
decoupledClock.ChangeSource(adjustableSourceClock);
|
||||
adjustableClock.ChangeSource(sourceClock);
|
||||
applyRateFromMods();
|
||||
|
||||
this.Delay(750).Schedule(() =>
|
||||
{
|
||||
if (!pauseContainer.IsPaused)
|
||||
decoupledClock.Start();
|
||||
{
|
||||
adjustableClock.Start();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -363,9 +369,7 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
|
||||
if (loadedSuccessfully)
|
||||
{
|
||||
pauseContainer?.Pause();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -24,7 +24,9 @@ namespace osu.Game.Screens.Play
|
||||
public class SkipButton : OverlayContainer, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private readonly double startTime;
|
||||
public IAdjustableClock AudioClock;
|
||||
|
||||
public IAdjustableClock AdjustableClock;
|
||||
public IFrameBasedClock FramedClock;
|
||||
|
||||
private Button button;
|
||||
private Box remainingTimeBox;
|
||||
@ -60,8 +62,11 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
var baseClock = Clock;
|
||||
|
||||
if (AudioClock != null)
|
||||
Clock = new FramedClock(AudioClock) { ProcessSourceClockFrames = false };
|
||||
if (FramedClock != null)
|
||||
{
|
||||
Clock = FramedClock;
|
||||
ProcessCustomClock = false;
|
||||
}
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -109,7 +114,7 @@ namespace osu.Game.Screens.Play
|
||||
using (BeginAbsoluteSequence(beginFadeTime))
|
||||
this.FadeOut(fade_time);
|
||||
|
||||
button.Action = () => AudioClock?.Seek(startTime - skip_required_cutoff - fade_time);
|
||||
button.Action = () => AdjustableClock?.Seek(startTime - skip_required_cutoff - fade_time);
|
||||
|
||||
displayTime = Time.Current;
|
||||
|
||||
|
@ -109,7 +109,7 @@ namespace osu.Game.Tests.Beatmaps
|
||||
|
||||
private Beatmap getBeatmap(string name)
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder();
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
using (var resStream = openResource($"{resource_namespace}.{name}.osu"))
|
||||
using (var stream = new StreamReader(resStream))
|
||||
return decoder.DecodeBeatmap(stream);
|
||||
|
@ -365,6 +365,11 @@
|
||||
<Compile Include="Overlays\Social\SocialPanel.cs" />
|
||||
<Compile Include="Rulesets\Mods\IApplicableToDrawableHitObject.cs" />
|
||||
<Compile Include="Rulesets\Objects\HitWindows.cs" />
|
||||
<Compile Include="Rulesets\Replays\Legacy\LegacyReplayFrame.cs" />
|
||||
<Compile Include="Rulesets\Replays\Legacy\ReplayButtonState.cs" />
|
||||
<Compile Include="Rulesets\Replays\ReplayFrame.cs" />
|
||||
<Compile Include="Rulesets\Replays\Types\IConvertibleReplayFrame.cs" />
|
||||
<Compile Include="Rulesets\Scoring\Legacy\LegacyScoreParser.cs" />
|
||||
<Compile Include="Rulesets\UI\ScalableContainer.cs" />
|
||||
<Compile Include="Screens\Play\PlayerSettings\VisualSettings.cs" />
|
||||
<Compile Include="Rulesets\Objects\CatmullApproximator.cs" />
|
||||
@ -710,8 +715,6 @@
|
||||
<Compile Include="Rulesets\Replays\FramedReplayInputHandler.cs" />
|
||||
<Compile Include="Rulesets\Replays\IAutoGenerator.cs" />
|
||||
<Compile Include="Rulesets\Replays\Replay.cs" />
|
||||
<Compile Include="Rulesets\Replays\ReplayButtonState.cs" />
|
||||
<Compile Include="Rulesets\Replays\ReplayFrame.cs" />
|
||||
<Compile Include="Rulesets\Ruleset.cs" />
|
||||
<Compile Include="Rulesets\RulesetInfo.cs" />
|
||||
<Compile Include="Rulesets\RulesetStore.cs" />
|
||||
|
Reference in New Issue
Block a user