From 58ae9e888d63652c4699aeed9a5c32e19bf08441 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Feb 2017 20:14:48 +0900 Subject: [PATCH] Basic partial replay support. --- osu-framework | 2 +- .../Tests/TestCaseReplay.cs | 15 + .../osu.Desktop.VisualTests.csproj | 1 + osu.Game.Modes.Catch/CatchRuleset.cs | 7 +- osu.Game.Modes.Mania/ManiaRuleset.cs | 7 +- osu.Game.Modes.Osu/OsuRuleset.cs | 7 +- osu.Game.Modes.Osu/UI/OsuPlayfield.cs | 4 +- osu.Game.Modes.Taiko/TaikoRuleset.cs | 7 +- .../Graphics/Cursor/OsuCursorContainer.cs | 4 +- osu.Game/IO/Legacy/ILegacySerializable.cs | 11 + osu.Game/IO/Legacy/SerializationReader.cs | 279 ++++++++++++++++++ osu.Game/IO/Legacy/SerializationWriter.cs | 255 ++++++++++++++++ .../Handlers/LegacyReplayInputHandler.cs | 244 +++++++++++++++ osu.Game/Modes/Ruleset.cs | 3 +- osu.Game/Modes/UI/HitRenderer.cs | 11 +- osu.Game/Modes/UI/Playfield.cs | 32 +- osu.Game/Screens/Play/Player.cs | 39 ++- osu.Game/Screens/Play/PlayerInputManager.cs | 92 +++++- osu.Game/osu.Game.csproj | 4 + 19 files changed, 984 insertions(+), 40 deletions(-) create mode 100644 osu.Desktop.VisualTests/Tests/TestCaseReplay.cs create mode 100644 osu.Game/IO/Legacy/ILegacySerializable.cs create mode 100644 osu.Game/IO/Legacy/SerializationReader.cs create mode 100644 osu.Game/IO/Legacy/SerializationWriter.cs create mode 100644 osu.Game/Input/Handlers/LegacyReplayInputHandler.cs diff --git a/osu-framework b/osu-framework index 4c0762eec2..b32d1542d4 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 4c0762eec20d2a3063df908a49432326570bea9f +Subproject commit b32d1542d45c02c39e91bd0ebf8cc79aade9dd63 diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs new file mode 100644 index 0000000000..1a95848500 --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace osu.Desktop.VisualTests.Tests +{ + class TestCaseReplay + { + } +} diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index c3234d7a96..0f8c7031de 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -186,6 +186,7 @@ + diff --git a/osu.Game.Modes.Catch/CatchRuleset.cs b/osu.Game.Modes.Catch/CatchRuleset.cs index fd778d1ce6..338129877b 100644 --- a/osu.Game.Modes.Catch/CatchRuleset.cs +++ b/osu.Game.Modes.Catch/CatchRuleset.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Modes.Catch.UI; using osu.Game.Modes.Objects; @@ -14,7 +15,11 @@ namespace osu.Game.Modes.Catch { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new CatchHitRenderer { Beatmap = beatmap }; + public override HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null) => new CatchHitRenderer + { + Beatmap = beatmap, + InputManager = input, + }; protected override PlayMode PlayMode => PlayMode.Catch; diff --git a/osu.Game.Modes.Mania/ManiaRuleset.cs b/osu.Game.Modes.Mania/ManiaRuleset.cs index bbf22086c0..1fb4d055a4 100644 --- a/osu.Game.Modes.Mania/ManiaRuleset.cs +++ b/osu.Game.Modes.Mania/ManiaRuleset.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Modes.Mania.UI; using osu.Game.Modes.Objects; @@ -14,7 +15,11 @@ namespace osu.Game.Modes.Mania { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new ManiaHitRenderer { Beatmap = beatmap }; + public override HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null) => new ManiaHitRenderer + { + Beatmap = beatmap, + InputManager = input, + }; protected override PlayMode PlayMode => PlayMode.Mania; diff --git a/osu.Game.Modes.Osu/OsuRuleset.cs b/osu.Game.Modes.Osu/OsuRuleset.cs index d05c8193cc..068b317fc6 100644 --- a/osu.Game.Modes.Osu/OsuRuleset.cs +++ b/osu.Game.Modes.Osu/OsuRuleset.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Modes.Objects; @@ -16,7 +17,11 @@ namespace osu.Game.Modes.Osu { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new OsuHitRenderer { Beatmap = beatmap }; + public override HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null) => new OsuHitRenderer + { + Beatmap = beatmap, + InputManager = input + }; public override IEnumerable GetBeatmapStatistics(WorkingBeatmap beatmap) => new[] { diff --git a/osu.Game.Modes.Osu/UI/OsuPlayfield.cs b/osu.Game.Modes.Osu/UI/OsuPlayfield.cs index 20164060fe..7d439a96ef 100644 --- a/osu.Game.Modes.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Modes.Osu/UI/OsuPlayfield.cs @@ -10,6 +10,7 @@ using osu.Game.Modes.Osu.Objects.Drawables; using osu.Game.Modes.Osu.Objects.Drawables.Connections; using osu.Game.Modes.UI; using System.Linq; +using osu.Game.Graphics.Cursor; namespace osu.Game.Modes.Osu.UI { @@ -53,7 +54,8 @@ namespace osu.Game.Modes.Osu.UI { RelativeSizeAxes = Axes.Both, Depth = -1, - } + }, + new OsuCursorContainer() }); } diff --git a/osu.Game.Modes.Taiko/TaikoRuleset.cs b/osu.Game.Modes.Taiko/TaikoRuleset.cs index 6141838880..9c1cef3530 100644 --- a/osu.Game.Modes.Taiko/TaikoRuleset.cs +++ b/osu.Game.Modes.Taiko/TaikoRuleset.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.UI; @@ -14,7 +15,11 @@ namespace osu.Game.Modes.Taiko { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(Beatmap beatmap) => new TaikoHitRenderer { Beatmap = beatmap }; + public override HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null) => new TaikoHitRenderer + { + Beatmap = beatmap, + InputManager = input, + }; protected override PlayMode PlayMode => PlayMode.Taiko; diff --git a/osu.Game/Graphics/Cursor/OsuCursorContainer.cs b/osu.Game/Graphics/Cursor/OsuCursorContainer.cs index ab681845af..ed3837ec13 100644 --- a/osu.Game/Graphics/Cursor/OsuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuCursorContainer.cs @@ -17,7 +17,7 @@ using System; namespace osu.Game.Graphics.Cursor { - class OsuCursorContainer : CursorContainer + public class OsuCursorContainer : CursorContainer { protected override Drawable CreateCursor() => new OsuCursor(); @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.Cursor return base.OnMouseUp(state, args); } - class OsuCursor : Container + public class OsuCursor : Container { private Container cursorContainer; private Bindable cursorScale; diff --git a/osu.Game/IO/Legacy/ILegacySerializable.cs b/osu.Game/IO/Legacy/ILegacySerializable.cs new file mode 100644 index 0000000000..a280a5d13a --- /dev/null +++ b/osu.Game/IO/Legacy/ILegacySerializable.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.IO.Legacy +{ + public interface ILegacySerializable + { + void ReadFromStream(SerializationReader sr); + void WriteToStream(SerializationWriter sw); + } +} \ No newline at end of file diff --git a/osu.Game/IO/Legacy/SerializationReader.cs b/osu.Game/IO/Legacy/SerializationReader.cs new file mode 100644 index 0000000000..10ba95167b --- /dev/null +++ b/osu.Game/IO/Legacy/SerializationReader.cs @@ -0,0 +1,279 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; +using System.Threading; + +namespace osu.Game.IO.Legacy +{ + /// SerializationReader. Extends BinaryReader to add additional data types, + /// handle null strings and simplify use with ISerializable. + public class SerializationReader : BinaryReader + { + Stream stream; + + public SerializationReader(Stream s) + : base(s, Encoding.UTF8) + { + stream = s; + } + + public int RemainingBytes => (int)(stream.Length - stream.Position); + + /// Static method to take a SerializationInfo object (an input to an ISerializable constructor) + /// and produce a SerializationReader from which serialized objects can be read . + public static SerializationReader GetReader(SerializationInfo info) + { + byte[] byteArray = (byte[])info.GetValue("X", typeof(byte[])); + MemoryStream ms = new MemoryStream(byteArray); + return new SerializationReader(ms); + } + + /// Reads a string from the buffer. Overrides the base implementation so it can cope with nulls. + public override string ReadString() + { + if (0 == ReadByte()) return null; + return base.ReadString(); + } + + /// Reads a byte array from the buffer, handling nulls and the array length. + public byte[] ReadByteArray() + { + int len = ReadInt32(); + if (len > 0) return ReadBytes(len); + if (len < 0) return null; + return new byte[0]; + } + + /// Reads a char array from the buffer, handling nulls and the array length. + public char[] ReadCharArray() + { + int len = ReadInt32(); + if (len > 0) return ReadChars(len); + if (len < 0) return null; + return new char[0]; + } + + /// Reads a DateTime from the buffer. + public DateTime ReadDateTime() + { + long ticks = ReadInt64(); + if (ticks < 0) throw new AbandonedMutexException("oops"); + return new DateTime(ticks, DateTimeKind.Utc); + } + + /// Reads a generic list from the buffer. + public IList ReadBList(bool skipErrors = false) where T : ILegacySerializable, new() + { + int count = ReadInt32(); + if (count < 0) return null; + IList d = new List(count); + + SerializationReader sr = new SerializationReader(BaseStream); + + for (int i = 0; i < count; i++) + { + T obj = new T(); + try + { + obj.ReadFromStream(sr); + } + catch (Exception) + { + if (skipErrors) + continue; + throw; + } + + d.Add(obj); + } + + return d; + } + + /// Reads a generic list from the buffer. + public IList ReadList() + { + int count = ReadInt32(); + if (count < 0) return null; + IList d = new List(count); + for (int i = 0; i < count; i++) d.Add((T)ReadObject()); + return d; + } + + /// Reads a generic Dictionary from the buffer. + public IDictionary ReadDictionary() + { + int count = ReadInt32(); + if (count < 0) return null; + IDictionary d = new Dictionary(); + for (int i = 0; i < count; i++) d[(T)ReadObject()] = (U)ReadObject(); + return d; + } + + /// Reads an object which was added to the buffer by WriteObject. + public object ReadObject() + { + ObjType t = (ObjType)ReadByte(); + switch (t) + { + case ObjType.boolType: + return ReadBoolean(); + case ObjType.byteType: + return ReadByte(); + case ObjType.uint16Type: + return ReadUInt16(); + case ObjType.uint32Type: + return ReadUInt32(); + case ObjType.uint64Type: + return ReadUInt64(); + case ObjType.sbyteType: + return ReadSByte(); + case ObjType.int16Type: + return ReadInt16(); + case ObjType.int32Type: + return ReadInt32(); + case ObjType.int64Type: + return ReadInt64(); + case ObjType.charType: + return ReadChar(); + case ObjType.stringType: + return base.ReadString(); + case ObjType.singleType: + return ReadSingle(); + case ObjType.doubleType: + return ReadDouble(); + case ObjType.decimalType: + return ReadDecimal(); + case ObjType.dateTimeType: + return ReadDateTime(); + case ObjType.byteArrayType: + return ReadByteArray(); + case ObjType.charArrayType: + return ReadCharArray(); + case ObjType.otherType: + return DynamicDeserializer.Deserialize(BaseStream); + default: + return null; + } + } + + public class DynamicDeserializer + { + private static VersionConfigToNamespaceAssemblyObjectBinder versionBinder; + private static BinaryFormatter formatter; + + private static void initialize() + { + versionBinder = new VersionConfigToNamespaceAssemblyObjectBinder(); + formatter = new BinaryFormatter(); + formatter.AssemblyFormat = FormatterAssemblyStyle.Simple; + formatter.Binder = versionBinder; + } + + public static object Deserialize(Stream stream) + { + if (formatter == null) + initialize(); + return formatter.Deserialize(stream); + } + + #region Nested type: VersionConfigToNamespaceAssemblyObjectBinder + + public sealed class VersionConfigToNamespaceAssemblyObjectBinder : SerializationBinder + { + private readonly Dictionary cache = new Dictionary(); + + public override Type BindToType(string assemblyName, string typeName) + { + Type typeToDeserialize; + + if (cache.TryGetValue(assemblyName + typeName, out typeToDeserialize)) + return typeToDeserialize; + + List tmpTypes = new List(); + Type genType = null; + + try + { + if (typeName.Contains("System.Collections.Generic") && typeName.Contains("[[")) + { + string[] splitTyps = typeName.Split('['); + + foreach (string typ in splitTyps) + { + if (typ.Contains("Version")) + { + string asmTmp = typ.Substring(typ.IndexOf(',') + 1); + string asmName = asmTmp.Remove(asmTmp.IndexOf(']')).Trim(); + string typName = typ.Remove(typ.IndexOf(',')); + tmpTypes.Add(BindToType(asmName, typName)); + } + else if (typ.Contains("Generic")) + { + genType = BindToType(assemblyName, typ); + } + } + if (genType != null && tmpTypes.Count > 0) + { + return genType.MakeGenericType(tmpTypes.ToArray()); + } + } + + string toAssemblyName = assemblyName.Split(',')[0]; + Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); + foreach (Assembly a in assemblies) + { + if (a.FullName.Split(',')[0] == toAssemblyName) + { + typeToDeserialize = a.GetType(typeName); + break; + } + } + } + catch (Exception exception) + { + throw exception; + } + + cache.Add(assemblyName + typeName, typeToDeserialize); + + return typeToDeserialize; + } + } + + #endregion + } + } + + public enum ObjType : byte + { + nullType, + boolType, + byteType, + uint16Type, + uint32Type, + uint64Type, + sbyteType, + int16Type, + int32Type, + int64Type, + charType, + stringType, + singleType, + doubleType, + decimalType, + dateTimeType, + byteArrayType, + charArrayType, + otherType, + ILegacySerializableType + } +} diff --git a/osu.Game/IO/Legacy/SerializationWriter.cs b/osu.Game/IO/Legacy/SerializationWriter.cs new file mode 100644 index 0000000000..df5facbc5b --- /dev/null +++ b/osu.Game/IO/Legacy/SerializationWriter.cs @@ -0,0 +1,255 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; + +namespace osu.Game.IO.Legacy +{ + /// SerializationWriter. Extends BinaryWriter to add additional data types, + /// handle null strings and simplify use with ISerializable. + public class SerializationWriter : BinaryWriter + { + public SerializationWriter(Stream s) + : base(s, Encoding.UTF8) + { + } + + /// Static method to initialise the writer with a suitable MemoryStream. + public static SerializationWriter GetWriter() + { + MemoryStream ms = new MemoryStream(1024); + return new SerializationWriter(ms); + } + + /// Writes a string to the buffer. Overrides the base implementation so it can cope with nulls + public override void Write(string str) + { + if (str == null) + { + Write((byte)ObjType.nullType); + } + else + { + Write((byte)ObjType.stringType); + base.Write(str); + } + } + + /// Writes a byte array to the buffer. Overrides the base implementation to + /// send the length of the array which is needed when it is retrieved + public override void Write(byte[] b) + { + if (b == null) + { + Write(-1); + } + else + { + int len = b.Length; + Write(len); + if (len > 0) base.Write(b); + } + } + + /// Writes a char array to the buffer. Overrides the base implementation to + /// sends the length of the array which is needed when it is read. + public override void Write(char[] c) + { + if (c == null) + { + Write(-1); + } + else + { + int len = c.Length; + Write(len); + if (len > 0) base.Write(c); + } + } + + /// Writes a DateTime to the buffer. + public void Write(DateTime dt) + { + Write(dt.ToUniversalTime().Ticks); + } + + /// Writes a generic ICollection (such as an IList) to the buffer. + public void Write(List c) where T : ILegacySerializable + { + if (c == null) + { + Write(-1); + } + else + { + int count = c.Count; + Write(count); + for (int i = 0; i < count; i++) + c[i].WriteToStream(this); + } + } + + /// Writes a generic IDictionary to the buffer. + public void Write(IDictionary d) + { + if (d == null) + { + Write(-1); + } + else + { + Write(d.Count); + foreach (KeyValuePair kvp in d) + { + WriteObject(kvp.Key); + WriteObject(kvp.Value); + } + } + } + + /// Writes an arbitrary object to the buffer. Useful where we have something of type "object" + /// and don't know how to treat it. This works out the best method to use to write to the buffer. + public void WriteObject(object obj) + { + if (obj == null) + { + Write((byte)ObjType.nullType); + } + else + { + switch (obj.GetType().Name) + { + case "Boolean": + Write((byte)ObjType.boolType); + Write((bool)obj); + break; + + case "Byte": + Write((byte)ObjType.byteType); + Write((byte)obj); + break; + + case "UInt16": + Write((byte)ObjType.uint16Type); + Write((ushort)obj); + break; + + case "UInt32": + Write((byte)ObjType.uint32Type); + Write((uint)obj); + break; + + case "UInt64": + Write((byte)ObjType.uint64Type); + Write((ulong)obj); + break; + + case "SByte": + Write((byte)ObjType.sbyteType); + Write((sbyte)obj); + break; + + case "Int16": + Write((byte)ObjType.int16Type); + Write((short)obj); + break; + + case "Int32": + Write((byte)ObjType.int32Type); + Write((int)obj); + break; + + case "Int64": + Write((byte)ObjType.int64Type); + Write((long)obj); + break; + + case "Char": + Write((byte)ObjType.charType); + base.Write((char)obj); + break; + + case "String": + Write((byte)ObjType.stringType); + base.Write((string)obj); + break; + + case "Single": + Write((byte)ObjType.singleType); + Write((float)obj); + break; + + case "Double": + Write((byte)ObjType.doubleType); + Write((double)obj); + break; + + case "Decimal": + Write((byte)ObjType.decimalType); + Write((decimal)obj); + break; + + case "DateTime": + Write((byte)ObjType.dateTimeType); + Write((DateTime)obj); + break; + + case "Byte[]": + Write((byte)ObjType.byteArrayType); + base.Write((byte[])obj); + break; + + case "Char[]": + Write((byte)ObjType.charArrayType); + base.Write((char[])obj); + break; + + default: + Write((byte)ObjType.otherType); + BinaryFormatter b = new BinaryFormatter(); + b.AssemblyFormat = FormatterAssemblyStyle.Simple; + b.TypeFormat = FormatterTypeStyle.TypesWhenNeeded; + b.Serialize(BaseStream, obj); + break; + } // switch + } // if obj==null + } // WriteObject + + /// Adds the SerializationWriter buffer to the SerializationInfo at the end of GetObjectData(). + public void AddToInfo(SerializationInfo info) + { + byte[] b = ((MemoryStream)BaseStream).ToArray(); + info.AddValue("X", b, typeof(byte[])); + } + + public void WriteRawBytes(byte[] b) + { + base.Write(b); + } + + public void WriteByteArray(byte[] b) + { + if (b == null) + { + Write(-1); + } + else + { + int len = b.Length; + Write(len); + if (len > 0) base.Write(b); + } + } + + public void WriteUtf8(string str) + { + WriteRawBytes(Encoding.UTF8.GetBytes(str)); + } + } +} \ No newline at end of file diff --git a/osu.Game/Input/Handlers/LegacyReplayInputHandler.cs b/osu.Game/Input/Handlers/LegacyReplayInputHandler.cs new file mode 100644 index 0000000000..03fa92515b --- /dev/null +++ b/osu.Game/Input/Handlers/LegacyReplayInputHandler.cs @@ -0,0 +1,244 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// 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.Input.Handlers; +using osu.Framework.MathUtils; +using osu.Framework.Platform; +using OpenTK; +using osu.Framework.Input; +using osu.Game.IO.Legacy; +using OpenTK.Input; +using KeyboardState = osu.Framework.Input.KeyboardState; +using MouseState = osu.Framework.Input.MouseState; + +namespace osu.Game.Input.Handlers +{ + /// + /// 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. + /// + public class LegacyReplayInputHandler : InputHandler + { + public Func ToScreenSpace { private get; set; } + + private readonly List replayContent; + int currentFrameIndex; + + public LegacyReplayFrame CurrentFrame => !hasFrames ? null : replayContent[currentFrameIndex]; + public LegacyReplayFrame NextFrame => !hasFrames ? null : replayContent[MathHelper.Clamp(currentDirection > 0 ? currentFrameIndex + 1 : currentFrameIndex - 1, 0, replayContent.Count - 1)]; + + public override bool Initialize(GameHost host) => true; + + public override bool IsActive => true; + + public override int Priority => 0; + + public LegacyReplayInputHandler(List replayContent) + { + this.replayContent = replayContent; + } + + private bool nextFrame() + { + int newFrame = MathHelper.Clamp(currentFrameIndex + (currentDirection > 0 ? 1 : -1), 0, replayContent.Count - 1); + + //ensure we aren't at an extent. + if (newFrame == currentFrameIndex) return false; + + currentFrameIndex = newFrame; + return true; + } + + public void SetPosition(Vector2 pos) + { + } + + private Vector2? position + { + get + { + if (!hasFrames) + return null; + + if (AtLastFrame) + return CurrentFrame.Position; + + return Interpolation.ValueAt(currentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time); + } + } + + public override List GetPendingStates() + { + return new List + { + new InputState + { + Mouse = new ReplayMouseState( + ToScreenSpace(position ?? Vector2.Zero), + new List + { + new MouseState.ButtonState(MouseButton.Left) { State = CurrentFrame?.MouseLeft ?? false }, + new MouseState.ButtonState(MouseButton.Right) { State = CurrentFrame?.MouseRight ?? false }, + } + ), + Keyboard = new ReplayKeyboardState(new List()) + } + }; + } + + public bool AtLastFrame => currentFrameIndex == replayContent.Count - 1; + public bool AtFirstFrame => currentFrameIndex == 0; + + public Vector2 Size => new Vector2(512, 384); + + private const double sixty_frame_time = 1000 / 60; + + double currentTime; + int currentDirection; + + /// + /// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data. + /// Disabling this can make replay playback smoother (useful for autoplay, currently). + /// + public bool FrameAccuratePlayback = true; + + private bool hasFrames => replayContent.Count > 0; + + bool inImportantSection => + FrameAccuratePlayback && + //a button is in a pressed state + (currentDirection > 0 ? CurrentFrame : NextFrame)?.ButtonState > LegacyButtonState.None && + //the next frame is within an allowable time span + Math.Abs(currentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2; + + /// + /// Update the current frame based on an incoming time value. + /// There are cases where we return a "must-use" time value that is different from the input. + /// This is to ensure accurate playback of replay data. + /// + /// The time which we should use for finding the current frame. + /// The usable time value. If null, we shouldn't be running components reliant on this data. + public double? SetFrameFromTime(double time) + { + currentDirection = time.CompareTo(currentTime); + if (currentDirection == 0) currentDirection = 1; + + if (hasFrames) + { + //if we changed frames, we want to execute once *exactly* on the frame's time. + if (currentDirection == time.CompareTo(NextFrame.Time) && nextFrame()) + return currentTime = CurrentFrame.Time; + + //if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null. + if (inImportantSection) + return null; + } + + return currentTime = time; + } + + private class ReplayMouseState : MouseState + { + public ReplayMouseState(Vector2 position, List list) + { + Position = position; + ButtonStates = list; + } + } + + private class ReplayKeyboardState : KeyboardState + { + public ReplayKeyboardState(List keys) + { + Keys = keys; + } + } + + [Flags] + public enum LegacyButtonState + { + None = 0, + Left1 = 1, + Right1 = 2, + Left2 = 4, + Right2 = 8, + Smoke = 16 + } + + public class LegacyReplayFrame + { + public Vector2 Position => new Vector2(MouseX, MouseY); + + public float MouseX; + public float MouseY; + public bool MouseLeft; + public bool MouseRight; + public bool MouseLeft1; + public bool MouseRight1; + public bool MouseLeft2; + public bool MouseRight2; + public LegacyButtonState ButtonState; + public double Time; + + public LegacyReplayFrame(double time, float posX, float posY, LegacyButtonState buttonState) + { + MouseX = posX; + MouseY = posY; + ButtonState = buttonState; + SetButtonStates(buttonState); + Time = time; + } + + public void SetButtonStates(LegacyButtonState buttonState) + { + ButtonState = buttonState; + MouseLeft = (buttonState & (LegacyButtonState.Left1 | LegacyButtonState.Left2)) > 0; + MouseLeft1 = (buttonState & LegacyButtonState.Left1) > 0; + MouseLeft2 = (buttonState & LegacyButtonState.Left2) > 0; + MouseRight = (buttonState & (LegacyButtonState.Right1 | LegacyButtonState.Right2)) > 0; + MouseRight1 = (buttonState & LegacyButtonState.Right1) > 0; + MouseRight2 = (buttonState & LegacyButtonState.Right2) > 0; + } + + public LegacyReplayFrame(Stream s) : this(new SerializationReader(s)) + { + } + + public LegacyReplayFrame(SerializationReader sr) + { + ButtonState = (LegacyButtonState)sr.ReadByte(); + SetButtonStates(ButtonState); + + byte bt = sr.ReadByte(); + if (bt > 0)//Handle Pre-Taiko compatible replays. + SetButtonStates(LegacyButtonState.Right1); + + MouseX = sr.ReadSingle(); + MouseY = sr.ReadSingle(); + Time = sr.ReadInt32(); + } + + public void ReadFromStream(SerializationReader sr) + { + throw new System.NotImplementedException(); + } + + public void WriteToStream(SerializationWriter sw) + { + sw.Write((byte)ButtonState); + sw.Write((byte)0); + sw.Write(MouseX); + sw.Write(MouseY); + sw.Write(Time); + } + + public override string ToString() + { + return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}"; + } + } + } +} \ No newline at end of file diff --git a/osu.Game/Modes/Ruleset.cs b/osu.Game/Modes/Ruleset.cs index 851e144408..9bff12d0ef 100644 --- a/osu.Game/Modes/Ruleset.cs +++ b/osu.Game/Modes/Ruleset.cs @@ -6,6 +6,7 @@ using osu.Game.Modes.Objects; using osu.Game.Modes.UI; using System; using System.Collections.Concurrent; +using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -28,7 +29,7 @@ namespace osu.Game.Modes public abstract ScoreProcessor CreateScoreProcessor(int hitObjectCount); - public abstract HitRenderer CreateHitRendererWith(Beatmap beatmap); + public abstract HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null); public abstract HitObjectParser CreateHitObjectParser(); diff --git a/osu.Game/Modes/UI/HitRenderer.cs b/osu.Game/Modes/UI/HitRenderer.cs index 14d9599be6..356c9b9276 100644 --- a/osu.Game/Modes/UI/HitRenderer.cs +++ b/osu.Game/Modes/UI/HitRenderer.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; using osu.Game.Beatmaps; @@ -19,6 +20,8 @@ namespace osu.Game.Modes.UI public event Action OnAllJudged; + public InputManager InputManager; + protected void TriggerOnJudgement(JudgementInfo j) { OnJudgement?.Invoke(j); @@ -62,10 +65,10 @@ namespace osu.Game.Modes.UI [BackgroundDependencyLoader] private void load() { - Children = new Drawable[] - { - Playfield = CreatePlayfield() - }; + Playfield = CreatePlayfield(); + Playfield.InputManager = InputManager; + + Add(Playfield); loadObjects(); } diff --git a/osu.Game/Modes/UI/Playfield.cs b/osu.Game/Modes/UI/Playfield.cs index 91eddce73c..6a8c311261 100644 --- a/osu.Game/Modes/UI/Playfield.cs +++ b/osu.Game/Modes/UI/Playfield.cs @@ -1,9 +1,11 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Game.Modes.Objects.Drawables; namespace osu.Game.Modes.UI @@ -11,7 +13,7 @@ namespace osu.Game.Modes.UI public abstract class Playfield : Container { public HitObjectContainer HitObjects; - private Container content; + private Container scaledContent; public virtual void Add(DrawableHitObject h) => HitObjects.Add(h); @@ -19,11 +21,20 @@ namespace osu.Game.Modes.UI protected override Container Content => content; + private Container content; + public Playfield() { - AddInternal(content = new ScaledContainer() + AddInternal(scaledContent = new ScaledContainer { RelativeSizeAxes = Axes.Both, + Children = new[] + { + content = new Container + { + RelativeSizeAxes = Axes.Both, + } + } }); Add(HitObjects = new HitObjectContainer @@ -32,6 +43,23 @@ namespace osu.Game.Modes.UI }); } + /// + /// An optional inputManager to provide interactivity etc. + /// + public InputManager InputManager; + + [BackgroundDependencyLoader] + private void load() + { + if (InputManager != null) + { + //if we've been provided an InputManager, we want it to sit inside the scaledcontainer + scaledContent.Remove(content); + scaledContent.Add(InputManager); + InputManager.Add(content); + } + } + public virtual void PostProcess() { } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 46f93900fc..e1c99ea82c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -19,12 +19,16 @@ using osu.Game.Configuration; using osu.Game.Overlays.Pause; using osu.Framework.Configuration; using System; +using System.Collections.Generic; using System.Linq; using OpenTK.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; +using osu.Framework.Input; using osu.Framework.Logging; using osu.Framework.Input; +using osu.Game.Graphics.Cursor; +using osu.Game.Input.Handlers; namespace osu.Game.Screens.Play { @@ -57,6 +61,7 @@ namespace osu.Game.Screens.Play private bool canPause => Time.Current >= (lastPauseActionTime + pauseCooldown); private IAdjustableClock sourceClock; + private IFrameBasedClock interpolatedSourceClock; private Ruleset ruleset; @@ -67,7 +72,6 @@ namespace osu.Game.Screens.Play private ScoreOverlay scoreOverlay; private PauseOverlay pauseOverlay; - private PlayerInputManager playerInputManager; [BackgroundDependencyLoader] private void load(AudioManager audio, BeatmapDatabase beatmaps, OsuGameBase game, OsuConfigManager config) @@ -101,6 +105,7 @@ namespace osu.Game.Screens.Play } sourceClock = (IAdjustableClock)track ?? new StopwatchClock(); + interpolatedSourceClock = new InterpolatingFramedClock(sourceClock); Schedule(() => { @@ -135,7 +140,20 @@ namespace osu.Game.Screens.Play OnQuit = Exit }; - hitRenderer = ruleset.CreateHitRendererWith(beatmap); + hitRenderer = ruleset.CreateHitRendererWith(beatmap, new PlayerInputManager + { + ReplayInputHandler = new LegacyReplayInputHandler(new List + { + new LegacyReplayInputHandler.LegacyReplayFrame(0, 0, 0, LegacyReplayInputHandler.LegacyButtonState.None), + new LegacyReplayInputHandler.LegacyReplayFrame(500, 512, 0, LegacyReplayInputHandler.LegacyButtonState.None), + new LegacyReplayInputHandler.LegacyReplayFrame(1000, 512, 384, LegacyReplayInputHandler.LegacyButtonState.None), + new LegacyReplayInputHandler.LegacyReplayFrame(1500, 0, 384, LegacyReplayInputHandler.LegacyButtonState.None), + new LegacyReplayInputHandler.LegacyReplayFrame(2000, 0, 0, LegacyReplayInputHandler.LegacyButtonState.None), + new LegacyReplayInputHandler.LegacyReplayFrame(2500, 512, 0, LegacyReplayInputHandler.LegacyButtonState.None), + new LegacyReplayInputHandler.LegacyReplayFrame(3000, 512, 384, LegacyReplayInputHandler.LegacyButtonState.None), + new LegacyReplayInputHandler.LegacyReplayFrame(3500, 0, 384, LegacyReplayInputHandler.LegacyButtonState.None), + }) + }); //bind HitRenderer to ScoreProcessor and ourselves (for a pass situation) hitRenderer.OnJudgement += scoreProcessor.AddJudgement; @@ -149,14 +167,17 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { - playerInputManager = new PlayerInputManager(game.Host) + new Container { - Clock = new InterpolatingFramedClock(sourceClock), - PassThrough = false, + RelativeSizeAxes = Axes.Both, + Clock = interpolatedSourceClock, Children = new Drawable[] { hitRenderer, - skipButton = new SkipButton { Alpha = 0 }, + skipButton = new SkipButton + { + Alpha = 0 + }, } }, scoreOverlay, @@ -196,7 +217,6 @@ namespace osu.Game.Screens.Play if (canPause || force) { lastPauseActionTime = Time.Current; - playerInputManager.PassThrough = true; scoreOverlay.KeyCounter.IsCounting = false; pauseOverlay.Retries = RestartCount; pauseOverlay.Show(); @@ -212,7 +232,6 @@ namespace osu.Game.Screens.Play public void Resume() { lastPauseActionTime = Time.Current; - playerInputManager.PassThrough = false; scoreOverlay.KeyCounter.IsCounting = true; pauseOverlay.Hide(); sourceClock.Start(); @@ -238,8 +257,8 @@ namespace osu.Game.Screens.Play if (!Push(newPlayer)) { - // Error(?) - } + // Error(?) + } }); } diff --git a/osu.Game/Screens/Play/PlayerInputManager.cs b/osu.Game/Screens/Play/PlayerInputManager.cs index 181cd68da9..084c96a82f 100644 --- a/osu.Game/Screens/Play/PlayerInputManager.cs +++ b/osu.Game/Screens/Play/PlayerInputManager.cs @@ -1,46 +1,75 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Input; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Input; -using osu.Framework.Platform; using osu.Game.Configuration; using System.Linq; +using osu.Framework.Timing; +using osu.Game.Input.Handlers; +using OpenTK.Input; +using KeyboardState = osu.Framework.Input.KeyboardState; +using MouseState = osu.Framework.Input.MouseState; namespace osu.Game.Screens.Play { - class PlayerInputManager : UserInputManager + public class PlayerInputManager : PassThroughInputManager { - public PlayerInputManager(GameHost host) - : base(host) + private bool leftViaKeyboard; + private bool rightViaKeyboard; + private Bindable mouseDisabled; + + private ManualClock clock = new ManualClock(); + private IFrameBasedClock parentClock; + + private LegacyReplayInputHandler replayInputHandler; + public LegacyReplayInputHandler ReplayInputHandler { + get { return replayInputHandler; } + set + { + if (replayInputHandler != null) RemoveHandler(replayInputHandler); + + replayInputHandler = value; + UseParentState = replayInputHandler == null; + + if (replayInputHandler != null) + { + replayInputHandler.ToScreenSpace = ToScreenSpace; + AddHandler(replayInputHandler); + } + } } - bool leftViaKeyboard; - bool rightViaKeyboard; - Bindable mouseDisabled; + protected override void LoadComplete() + { + base.LoadComplete(); + + parentClock = Clock; + Clock = new FramedClock(clock); + } [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - mouseDisabled = config.GetBindable(OsuConfig.MouseDisableButtons) - ?? new Bindable(false); + mouseDisabled = config.GetBindable(OsuConfig.MouseDisableButtons); } protected override void TransformState(InputState state) { base.TransformState(state); - if (state.Keyboard != null) + var mouse = state.Mouse as MouseState; + var keyboard = state.Keyboard as KeyboardState; + + if (keyboard != null) { - leftViaKeyboard = state.Keyboard.Keys.Contains(Key.Z); - rightViaKeyboard = state.Keyboard.Keys.Contains(Key.X); + leftViaKeyboard = keyboard.Keys.Contains(Key.Z); + rightViaKeyboard = keyboard.Keys.Contains(Key.X); } - var mouse = (Framework.Input.MouseState)state.Mouse; - if (state.Mouse != null) + if (mouse != null) { if (mouseDisabled.Value) { @@ -54,5 +83,38 @@ namespace osu.Game.Screens.Play mouse.ButtonStates.Find(s => s.Button == MouseButton.Right).State = true; } } + + protected override void Update() + { + base.Update(); + + if (parentClock == null) return; + + clock.Rate = parentClock.Rate; + clock.IsRunning = parentClock.IsRunning; + + //if a replayHandler is not attached, we should just pass-through. + if (UseParentState || replayInputHandler == null) + { + clock.CurrentTime = parentClock.CurrentTime; + base.Update(); + return; + } + + while (true) + { + double? newTime = replayInputHandler.SetFrameFromTime(parentClock.CurrentTime); + + if (newTime == null) + //we shouldn't execute for this time value + break; + + if (clock.CurrentTime == parentClock.CurrentTime) + break; + + clock.CurrentTime = newTime.Value; + base.Update(); + } + } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ddad06a476..71f0fc92a8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -79,6 +79,10 @@ + + + +