From 58ae9e888d63652c4699aeed9a5c32e19bf08441 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Feb 2017 20:14:48 +0900 Subject: [PATCH 01/34] 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 @@ + + + + From b6e7e054c33aa11bcf56e607f287005c7aaec393 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 1 Mar 2017 22:56:20 +0900 Subject: [PATCH 02/34] wankoz --- .../Tests/TestCaseReplay.cs | 100 +++++++++++++++++- .../osu.Desktop.VisualTests.csproj | 4 + osu.Desktop.VisualTests/packages.config | 1 + osu.Desktop/osu.Desktop.csproj | 4 +- osu.Desktop/packages.config | 1 + osu.Game/Screens/Play/Player.cs | 14 +-- 6 files changed, 107 insertions(+), 17 deletions(-) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs index 1a95848500..d089f3bca7 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs @@ -3,13 +3,105 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.IO; +using osu.Framework.Allocation; +using osu.Framework.Screens.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using OpenTK; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps.IO; +using osu.Game.Database; +using osu.Game.Input.Handlers; +using osu.Game.IO.Legacy; +using osu.Game.Modes; +using osu.Game.Modes.Objects; +using osu.Game.Modes.Osu.Objects; +using osu.Game.Screens.Play; +using OpenTK.Graphics; +using SharpCompress.Archives.SevenZip; +using SharpCompress.Readers; namespace osu.Desktop.VisualTests.Tests { - class TestCaseReplay + class TestCaseReplay : TestCase { + private WorkingBeatmap beatmap; + public override string Name => @"Replay"; + + public override string Description => @"Testing replay playback."; + + [BackgroundDependencyLoader] + private void load(BeatmapDatabase db) + { + var beatmapInfo = db.Query().Where(b => b.Mode == PlayMode.Osu).FirstOrDefault(); + if (beatmapInfo != null) + beatmap = db.GetWorkingBeatmap(beatmapInfo); + } + + public override void Reset() + { + base.Reset(); + + Add(new Box + { + RelativeSizeAxes = Framework.Graphics.Axes.Both, + Colour = Color4.Black, + }); + + + var list = new List(); + + float lastTime = 0; + foreach (var l in File.ReadAllText(@"C:\Users\Dean\Desktop\2157025197").Split(',')) + { + var split = l.Split('|'); + + if (split.Length < 4 || float.Parse(split[0]) < 0) continue; + + lastTime += float.Parse(split[0]); + + list.Add(new LegacyReplayInputHandler.LegacyReplayFrame( + lastTime, + float.Parse(split[1]), + 384 - float.Parse(split[2]), + (LegacyReplayInputHandler.LegacyButtonState)int.Parse(split[3]) + )); + } + + + var replay = new LegacyReplayInputHandler(list); + + //var data = File.ReadAllBytes(@"C:\Users\Dean\.osu\Replays\Tao - O2i3 - Ooi [Game Edit] [Advanced] (2016-08-08) Osu.osr"); + //using (MemoryStream dataStream = new MemoryStream(data)) + //{ + // var obj = SerializationReader.DynamicDeserializer.Deserialize(dataStream); + + + // Console.WriteLine(obj); + //} + + + Add(new PlayerLoader(new Player + { + PreferredPlayMode = PlayMode.Osu, + ReplayInputHandler = replay, + Beatmap = beatmap + }) + { + Beatmap = beatmap + }); + } + + class TestWorkingBeatmap : WorkingBeatmap + { + public TestWorkingBeatmap(Beatmap beatmap) + : base(beatmap.BeatmapInfo, beatmap.BeatmapInfo.BeatmapSet) + { + Beatmap = beatmap; + } + + protected override ArchiveReader GetReader() => null; + } } } diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index 0f8c7031de..2426ff9d2e 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -86,6 +86,10 @@ $(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1340\lib\net45\OpenTK.dll True + + ..\packages\SharpCompress.0.15.1\lib\net45\SharpCompress.dll + True + False $(SolutionDir)\packages\SQLite.Net.Core-PCL.3.1.1\lib\portable-win8+net45+wp8+wpa81+MonoAndroid1+MonoTouch1\SQLite.Net.dll diff --git a/osu.Desktop.VisualTests/packages.config b/osu.Desktop.VisualTests/packages.config index 3da209ee61..5a30c50600 100644 --- a/osu.Desktop.VisualTests/packages.config +++ b/osu.Desktop.VisualTests/packages.config @@ -6,6 +6,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 527a027ca2..7e85f09d8c 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -101,8 +101,8 @@ $(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.PatchApi.dll True - - $(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\ICSharpCode.SharpZipLib.dll + + ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll True diff --git a/osu.Desktop/packages.config b/osu.Desktop/packages.config index bdeaf1de89..35305fb5d8 100644 --- a/osu.Desktop/packages.config +++ b/osu.Desktop/packages.config @@ -7,6 +7,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste + \ No newline at end of file diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e1c99ea82c..98344e7017 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -142,17 +142,7 @@ namespace osu.Game.Screens.Play 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), - }) + ReplayInputHandler = ReplayInputHandler }); //bind HitRenderer to ScoreProcessor and ourselves (for a pass situation) @@ -350,6 +340,8 @@ namespace osu.Game.Screens.Play private Bindable mouseWheelDisabled; + public LegacyReplayInputHandler ReplayInputHandler; + protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value && !isPaused; } } \ No newline at end of file From 8040d6a03f4ae901ff67f024a443cdebcc92464f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Mar 2017 16:08:10 +0900 Subject: [PATCH 03/34] Fix CursorTrail corruption by resetting on load. --- osu.Game/Graphics/Cursor/CursorTrail.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/CursorTrail.cs b/osu.Game/Graphics/Cursor/CursorTrail.cs index 7e0937155c..e5b2c9acf9 100644 --- a/osu.Game/Graphics/Cursor/CursorTrail.cs +++ b/osu.Game/Graphics/Cursor/CursorTrail.cs @@ -32,7 +32,7 @@ namespace osu.Game.Graphics.Cursor private double timeOffset; private float time; - + private TrailDrawNodeSharedData trailDrawNodeSharedData = new TrailDrawNodeSharedData(); private const int max_sprites = 2048; @@ -77,6 +77,12 @@ namespace osu.Game.Graphics.Cursor Scale = new Vector2(1 / texture.ScaleAdjust); } + protected override void LoadComplete() + { + base.LoadComplete(); + resetTime(); + } + protected override void Update() { base.Update(); From 4bd85fe5cb0af735a27b4cfe4825722f31b19b32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 2 Mar 2017 16:08:37 +0900 Subject: [PATCH 04/34] Fix audio disposal issues and share more code between visualtests. --- .../Tests/TestCasePlayer.cs | 29 ++++++++---- .../Tests/TestCaseReplay.cs | 45 ++++--------------- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs index 7533a1a1e6..7548005ce4 100644 --- a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs +++ b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs @@ -20,7 +20,9 @@ namespace osu.Desktop.VisualTests.Tests { class TestCasePlayer : TestCase { - private WorkingBeatmap beatmap; + protected Player Player; + private BeatmapDatabase db; + public override string Name => @"Player"; public override string Description => @"Showing everything to play the game."; @@ -28,15 +30,19 @@ namespace osu.Desktop.VisualTests.Tests [BackgroundDependencyLoader] private void load(BeatmapDatabase db) { - var beatmapInfo = db.Query().Where(b => b.Mode == PlayMode.Osu).FirstOrDefault(); - if (beatmapInfo != null) - beatmap = db.GetWorkingBeatmap(beatmapInfo); + this.db = db; } public override void Reset() { base.Reset(); + WorkingBeatmap beatmap = null; + + var beatmapInfo = db.Query().Where(b => b.Mode == PlayMode.Osu).FirstOrDefault(); + if (beatmapInfo != null) + beatmap = db.GetWorkingBeatmap(beatmapInfo); + if (beatmap?.Track == null) { var objects = new List(); @@ -82,16 +88,21 @@ namespace osu.Desktop.VisualTests.Tests Colour = Color4.Black, }); - Add(new PlayerLoader(new Player - { - PreferredPlayMode = PlayMode.Osu, - Beatmap = beatmap - }) + Add(new PlayerLoader(Player = CreatePlayer(beatmap)) { Beatmap = beatmap }); } + protected virtual Player CreatePlayer(WorkingBeatmap beatmap) + { + return new Player + { + PreferredPlayMode = PlayMode.Osu, + Beatmap = beatmap + }; + } + class TestWorkingBeatmap : WorkingBeatmap { public TestWorkingBeatmap(Beatmap beatmap) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs index d089f3bca7..c5ca3b47c4 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs @@ -24,32 +24,20 @@ using SharpCompress.Readers; namespace osu.Desktop.VisualTests.Tests { - class TestCaseReplay : TestCase + class TestCaseReplay : TestCasePlayer { private WorkingBeatmap beatmap; + + private LegacyReplayInputHandler replay; + public override string Name => @"Replay"; public override string Description => @"Testing replay playback."; - [BackgroundDependencyLoader] - private void load(BeatmapDatabase db) - { - var beatmapInfo = db.Query().Where(b => b.Mode == PlayMode.Osu).FirstOrDefault(); - if (beatmapInfo != null) - beatmap = db.GetWorkingBeatmap(beatmapInfo); - } - public override void Reset() { base.Reset(); - Add(new Box - { - RelativeSizeAxes = Framework.Graphics.Axes.Both, - Colour = Color4.Black, - }); - - var list = new List(); float lastTime = 0; @@ -70,7 +58,7 @@ namespace osu.Desktop.VisualTests.Tests } - var replay = new LegacyReplayInputHandler(list); + replay = new LegacyReplayInputHandler(list); //var data = File.ReadAllBytes(@"C:\Users\Dean\.osu\Replays\Tao - O2i3 - Ooi [Game Edit] [Advanced] (2016-08-08) Osu.osr"); //using (MemoryStream dataStream = new MemoryStream(data)) @@ -80,28 +68,13 @@ namespace osu.Desktop.VisualTests.Tests // Console.WriteLine(obj); //} - - - Add(new PlayerLoader(new Player - { - PreferredPlayMode = PlayMode.Osu, - ReplayInputHandler = replay, - Beatmap = beatmap - }) - { - Beatmap = beatmap - }); } - class TestWorkingBeatmap : WorkingBeatmap + protected override Player CreatePlayer(WorkingBeatmap beatmap) { - public TestWorkingBeatmap(Beatmap beatmap) - : base(beatmap.BeatmapInfo, beatmap.BeatmapInfo.BeatmapSet) - { - Beatmap = beatmap; - } - - protected override ArchiveReader GetReader() => null; + var player = base.CreatePlayer(beatmap); + player.ReplayInputHandler = replay; + return player; } } } From adb6f01e39c237bf4a008df48a46c6447cad0d4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Mar 2017 15:29:15 +0900 Subject: [PATCH 05/34] Create class hierarchy for Score/Replay storage. --- .../Tests/TestCaseReplay.cs | 358 ++++++++++++++++-- osu.Game.Modes.Osu/OsuScoreProcessor.cs | 2 +- .../Handlers/LegacyReplayInputHandler.cs | 244 ------------ osu.Game/Input/Handlers/ReplayInputHandler.cs | 41 ++ osu.Game/Modes/Ruleset.cs | 2 +- osu.Game/Modes/Score.cs | 10 + osu.Game/Screens/Play/Player.cs | 3 +- osu.Game/Screens/Play/PlayerInputManager.cs | 5 +- osu.Game/osu.Game.csproj | 2 +- 9 files changed, 382 insertions(+), 285 deletions(-) delete mode 100644 osu.Game/Input/Handlers/LegacyReplayInputHandler.cs create mode 100644 osu.Game/Input/Handlers/ReplayInputHandler.cs diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs index c5ca3b47c4..fe69469db7 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs @@ -4,12 +4,17 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using osu.Framework.Allocation; using osu.Framework.Screens.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using OpenTK; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Framework.Input.Handlers; +using osu.Framework.MathUtils; +using osu.Framework.Platform; using osu.Game.Beatmaps.IO; using osu.Game.Database; using osu.Game.Input.Handlers; @@ -19,62 +24,345 @@ using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.Objects; using osu.Game.Screens.Play; using OpenTK.Graphics; +using OpenTK.Input; using SharpCompress.Archives.SevenZip; using SharpCompress.Readers; +using KeyboardState = osu.Framework.Input.KeyboardState; +using MouseState = osu.Framework.Input.MouseState; namespace osu.Desktop.VisualTests.Tests { + public class ScoreDatabase + { + private readonly Storage storage; + + private const string replay_folder = @"replays"; + + public ScoreDatabase(Storage storage) + { + this.storage = storage; + } + + public Score ReadReplayFile(string replayFilename) + { + Score score; + + using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename))) + using (SerializationReader sr = new SerializationReader(s)) + { + var ruleset = Ruleset.GetRuleset((PlayMode)sr.ReadByte()); + var processor = ruleset.CreateScoreProcessor(); + + score = processor.GetScore(); + + + + /* score.Pass = true;*/ + var version = sr.ReadInt32(); + /* score.FileChecksum = */ sr.ReadString(); + /* score.PlayerName = */ 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(); + + //new ASCIIEncoding().GetString(SevenZipHelper.Decompress(ReplayCompressed))); + + score.Replay = new LegacyReplay(); + + //float lastTime = 0; + //foreach (var l in File.ReadAllText(@"C:\Users\Dean\Desktop\2157025197").Split(',')) + //{ + // var split = l.Split('|'); + + // if (split.Length < 4 || float.Parse(split[0]) < 0) continue; + + // lastTime += float.Parse(split[0]); + + // list.Add(new LegacyReplay.LegacyReplayInputHandler.LegacyReplayFrame( + // lastTime, + // float.Parse(split[1]), + // 384 - float.Parse(split[2]), + // (LegacyReplay.LegacyReplayInputHandler.LegacyButtonState)int.Parse(split[3]) + // )); + //} + } + + return score; + } + } + class TestCaseReplay : TestCasePlayer { private WorkingBeatmap beatmap; - private LegacyReplayInputHandler replay; + private InputHandler replay; + + private Func getReplayStream; + private ScoreDatabase scoreDatabase; public override string Name => @"Replay"; public override string Description => @"Testing replay playback."; - public override void Reset() + [BackgroundDependencyLoader] + private void load(Storage storage) { - base.Reset(); - - var list = new List(); - - float lastTime = 0; - foreach (var l in File.ReadAllText(@"C:\Users\Dean\Desktop\2157025197").Split(',')) - { - var split = l.Split('|'); - - if (split.Length < 4 || float.Parse(split[0]) < 0) continue; - - lastTime += float.Parse(split[0]); - - list.Add(new LegacyReplayInputHandler.LegacyReplayFrame( - lastTime, - float.Parse(split[1]), - 384 - float.Parse(split[2]), - (LegacyReplayInputHandler.LegacyButtonState)int.Parse(split[3]) - )); - } - - - replay = new LegacyReplayInputHandler(list); - - //var data = File.ReadAllBytes(@"C:\Users\Dean\.osu\Replays\Tao - O2i3 - Ooi [Game Edit] [Advanced] (2016-08-08) Osu.osr"); - //using (MemoryStream dataStream = new MemoryStream(data)) - //{ - // var obj = SerializationReader.DynamicDeserializer.Deserialize(dataStream); - - - // Console.WriteLine(obj); - //} + scoreDatabase = new ScoreDatabase(storage); } protected override Player CreatePlayer(WorkingBeatmap beatmap) { var player = base.CreatePlayer(beatmap); - player.ReplayInputHandler = replay; + player.ReplayInputHandler = scoreDatabase.ReadReplayFile(@"Tao - O2i3 - Ooi [Game Edit] [Advanced] (2016-08-08) Osu.osr").Replay.GetInputHandler(); return player; } } + + public class LegacyReplay : Replay + { + private new List frames = new List(); + + public override ReplayInputHandler GetInputHandler() => new LegacyReplayInputHandler(frames); + + /// + /// 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 : ReplayInputHandler + { + 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 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 override 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}"; + } + } + } } diff --git a/osu.Game.Modes.Osu/OsuScoreProcessor.cs b/osu.Game.Modes.Osu/OsuScoreProcessor.cs index 9493259558..269047dd19 100644 --- a/osu.Game.Modes.Osu/OsuScoreProcessor.cs +++ b/osu.Game.Modes.Osu/OsuScoreProcessor.cs @@ -8,7 +8,7 @@ namespace osu.Game.Modes.Osu { class OsuScoreProcessor : ScoreProcessor { - public OsuScoreProcessor(int hitObjectCount) + public OsuScoreProcessor(int hitObjectCount = 0) : base(hitObjectCount) { Health.Value = 1; diff --git a/osu.Game/Input/Handlers/LegacyReplayInputHandler.cs b/osu.Game/Input/Handlers/LegacyReplayInputHandler.cs deleted file mode 100644 index 03fa92515b..0000000000 --- a/osu.Game/Input/Handlers/LegacyReplayInputHandler.cs +++ /dev/null @@ -1,244 +0,0 @@ -// 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/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs new file mode 100644 index 0000000000..d933c68099 --- /dev/null +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -0,0 +1,41 @@ +// 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 +{ + public abstract class ReplayInputHandler : InputHandler + { + /// + /// A function provided to convert replay coordinates from gamefield to screen space. + /// + public Func ToScreenSpace { protected get; set; } + + /// + /// 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 abstract double? SetFrameFromTime(double time); + + public override bool Initialize(GameHost host) => true; + + public override bool IsActive => true; + + public override int Priority => 0; + } +} \ No newline at end of file diff --git a/osu.Game/Modes/Ruleset.cs b/osu.Game/Modes/Ruleset.cs index 2b665dff17..30ac8e31ab 100644 --- a/osu.Game/Modes/Ruleset.cs +++ b/osu.Game/Modes/Ruleset.cs @@ -30,7 +30,7 @@ namespace osu.Game.Modes public abstract IEnumerable GetModsFor(ModType type); - public abstract ScoreProcessor CreateScoreProcessor(int hitObjectCount); + public abstract ScoreProcessor CreateScoreProcessor(int hitObjectCount = 0); public abstract HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null); diff --git a/osu.Game/Modes/Score.cs b/osu.Game/Modes/Score.cs index 8da09cb974..a1324f614f 100644 --- a/osu.Game/Modes/Score.cs +++ b/osu.Game/Modes/Score.cs @@ -1,6 +1,9 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Input.Handlers; +using osu.Game.Input.Handlers; + namespace osu.Game.Modes { public class Score @@ -10,5 +13,12 @@ namespace osu.Game.Modes public double Combo { get; set; } public double MaxCombo { get; set; } public double Health { get; set; } + + public Replay Replay; + } + + public class Replay + { + public virtual ReplayInputHandler GetInputHandler() => null; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 0aab0f9de7..e3651f0869 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -26,6 +26,7 @@ using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Framework.Logging; using osu.Framework.Input; +using osu.Framework.Input.Handlers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Handlers; @@ -339,7 +340,7 @@ namespace osu.Game.Screens.Play private Bindable mouseWheelDisabled; - public LegacyReplayInputHandler ReplayInputHandler; + public ReplayInputHandler ReplayInputHandler; protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value && !isPaused; } diff --git a/osu.Game/Screens/Play/PlayerInputManager.cs b/osu.Game/Screens/Play/PlayerInputManager.cs index 084c96a82f..ac98b6533b 100644 --- a/osu.Game/Screens/Play/PlayerInputManager.cs +++ b/osu.Game/Screens/Play/PlayerInputManager.cs @@ -6,6 +6,7 @@ using osu.Framework.Configuration; using osu.Framework.Input; using osu.Game.Configuration; using System.Linq; +using osu.Framework.Input.Handlers; using osu.Framework.Timing; using osu.Game.Input.Handlers; using OpenTK.Input; @@ -23,8 +24,8 @@ namespace osu.Game.Screens.Play private ManualClock clock = new ManualClock(); private IFrameBasedClock parentClock; - private LegacyReplayInputHandler replayInputHandler; - public LegacyReplayInputHandler ReplayInputHandler + private ReplayInputHandler replayInputHandler; + public ReplayInputHandler ReplayInputHandler { get { return replayInputHandler; } set diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 23ca656199..9851edd8c1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -79,7 +79,7 @@ - + From b29438607788c65e397b8e568d8fb9daf82aff02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Mar 2017 16:47:37 +0900 Subject: [PATCH 06/34] Remove misleading beatmap import method. --- osu.Desktop/OsuGameDesktop.cs | 3 ++- osu.Game/OsuGame.cs | 7 +------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index d9b9c31617..d3358da013 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -9,6 +9,7 @@ using osu.Framework.Desktop.Platform; using osu.Desktop.Overlays; using System.Reflection; using System.Drawing; +using System.Threading.Tasks; using osu.Game.Screens.Menu; namespace osu.Desktop @@ -56,7 +57,7 @@ namespace osu.Desktop // this method will only be executed if e.Effect in dragEnter gets set to something other that None. var dropData = e.Data.GetData(DataFormats.FileDrop) as object[]; var filePaths = dropData.Select(f => f.ToString()).ToArray(); - ImportBeatmapsAsync(filePaths); + Task.Run(() => BeatmapDatabase.Import(filePaths)); } private void dragEnter(DragEventArgs e) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 9f7e3e04dc..c56907c86d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -84,7 +84,7 @@ namespace osu.Game if (args?.Length > 0) { var paths = args.Where(a => !a.StartsWith(@"-")); - ImportBeatmapsAsync(paths); + Task.Run(() => BeatmapDatabase.Import(paths)); } Dependencies.Cache(this); @@ -92,11 +92,6 @@ namespace osu.Game PlayMode = LocalConfig.GetBindable(OsuConfig.PlayMode); } - protected async void ImportBeatmapsAsync(IEnumerable paths) - { - await Task.Run(() => BeatmapDatabase.Import(paths)); - } - protected override void LoadComplete() { base.LoadComplete(); From aa9d85624d59cad83fd31aa8a146e7fda14289a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Mar 2017 18:51:16 +0900 Subject: [PATCH 07/34] Change IPC to make sense. --- osu.Desktop/Program.cs | 2 +- .../Beatmaps/IO/ImportBeatmapTest.cs | 2 +- osu.Game/Database/BeatmapDatabase.cs | 8 ++-- osu.Game/IPC/BeatmapIPCChannel.cs | 43 +++++++++++++++++ osu.Game/IPC/BeatmapImporter.cs | 46 ------------------- osu.Game/osu.Game.csproj | 2 +- 6 files changed, 50 insertions(+), 53 deletions(-) create mode 100644 osu.Game/IPC/BeatmapIPCChannel.cs delete mode 100644 osu.Game/IPC/BeatmapImporter.cs diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 3171e474dc..ddf58ac363 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -29,7 +29,7 @@ namespace osu.Desktop { if (!host.IsPrimaryInstance) { - var importer = new BeatmapImporter(host); + var importer = new BeatmapIPCChannel(host); // Restore the cwd so relative paths given at the command line work correctly Directory.SetCurrentDirectory(cwd); foreach (var file in args) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 90d1aa542f..3132d63188 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(File.Exists(temp)); - var importer = new BeatmapImporter(client); + var importer = new BeatmapIPCChannel(client); if (!importer.ImportAsync(temp).Wait(1000)) Assert.Fail(@"IPC took too long to send"); diff --git a/osu.Game/Database/BeatmapDatabase.cs b/osu.Game/Database/BeatmapDatabase.cs index 03f904b7e8..df8ead7f94 100644 --- a/osu.Game/Database/BeatmapDatabase.cs +++ b/osu.Game/Database/BeatmapDatabase.cs @@ -25,14 +25,14 @@ namespace osu.Game.Database public event Action BeatmapSetAdded; public event Action BeatmapSetRemoved; - private BeatmapImporter ipc; + private BeatmapIPCChannel ipc; - public BeatmapDatabase(Storage storage, GameHost importHost = null) + public BeatmapDatabase(Storage storage, IIpcHost importHost = null) { this.storage = storage; if (importHost != null) - ipc = new BeatmapImporter(importHost, this); + ipc = new BeatmapIPCChannel(importHost, this); if (connection == null) { @@ -151,7 +151,7 @@ namespace osu.Game.Database e = e.InnerException ?? e; Logger.Error(e, $@"Could not import beatmap set"); } - + // Batch commit with multiple sets to database Import(sets); } diff --git a/osu.Game/IPC/BeatmapIPCChannel.cs b/osu.Game/IPC/BeatmapIPCChannel.cs new file mode 100644 index 0000000000..8c53910146 --- /dev/null +++ b/osu.Game/IPC/BeatmapIPCChannel.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Diagnostics; +using System.Threading.Tasks; +using osu.Framework.Platform; +using osu.Game.Database; + +namespace osu.Game.IPC +{ + public class BeatmapIPCChannel : IpcChannel + { + private BeatmapDatabase beatmaps; + + public BeatmapIPCChannel(IIpcHost host, BeatmapDatabase beatmaps = null) + : base(host) + { + this.beatmaps = beatmaps; + MessageReceived += (msg) => + { + Debug.Assert(beatmaps != null); + ImportAsync(msg.Path); + }; + } + + public async Task ImportAsync(string path) + { + if (beatmaps == null) + { + //we want to contact a remote osu! to handle the import. + await SendMessageAsync(new BeatmapImportMessage { Path = path }); + return; + } + + beatmaps.Import(path); + } + } + + public class BeatmapImportMessage + { + public string Path; + } +} diff --git a/osu.Game/IPC/BeatmapImporter.cs b/osu.Game/IPC/BeatmapImporter.cs deleted file mode 100644 index b6ce4d1e35..0000000000 --- a/osu.Game/IPC/BeatmapImporter.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Diagnostics; -using System.Threading.Tasks; -using osu.Framework.Platform; -using osu.Game.Database; - -namespace osu.Game.IPC -{ - public class BeatmapImporter - { - private IpcChannel channel; - private BeatmapDatabase beatmaps; - - public BeatmapImporter(GameHost host, BeatmapDatabase beatmaps = null) - { - this.beatmaps = beatmaps; - - channel = new IpcChannel(host); - channel.MessageReceived += messageReceived; - } - - public async Task ImportAsync(string path) - { - if (beatmaps != null) - beatmaps.Import(path); - else - { - await channel.SendMessageAsync(new BeatmapImportMessage { Path = path }); - } - } - - private void messageReceived(BeatmapImportMessage msg) - { - Debug.Assert(beatmaps != null); - - ImportAsync(msg.Path); - } - } - - public class BeatmapImportMessage - { - public string Path; - } -} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 9851edd8c1..f5c9b8723e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -190,7 +190,7 @@ - + From a8deb4ff50d0b82b45271bfc1e6c593d46e8dd43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Mar 2017 19:02:13 +0900 Subject: [PATCH 08/34] Fix WaveOverlayContainer always being visible. --- osu.Game/Overlays/WaveOverlayContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index fba6244d15..8c9c6a9943 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -135,6 +135,8 @@ namespace osu.Game.Overlays contentContainer.FadeIn(APPEAR_DURATION, EasingTypes.OutQuint); contentContainer.MoveToY(0, APPEAR_DURATION, EasingTypes.OutQuint); + + FadeIn(100, EasingTypes.OutQuint); } protected override void PopOut() @@ -146,6 +148,8 @@ namespace osu.Game.Overlays foreach (var w in wavesContainer.Children) w.State = Visibility.Hidden; + + FadeOut(DISAPPEAR_DURATION, EasingTypes.InQuint); } private class Wave : Container, IStateful From 95e2e2b027bfc4d4affe3dd58ceb41536a885f8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Mar 2017 19:02:36 +0900 Subject: [PATCH 09/34] Replay loading via drag-drop huzzah! --- osu-framework | 2 +- .../Tests/TestCaseReplay.cs | 308 +----------------- osu.Desktop/OsuGameDesktop.cs | 19 +- osu.Game/Database/BeatmapDatabase.cs | 25 +- osu.Game/Database/BeatmapInfo.cs | 3 + osu.Game/Database/ScoreDatabase.cs | 110 +++++++ osu.Game/IPC/ScoreIPCChannel.cs | 43 +++ osu.Game/Modes/LegacyReplay.cs | 269 +++++++++++++++ osu.Game/Modes/Replay.cs | 12 + osu.Game/Modes/Score.cs | 9 +- osu.Game/OsuGame.cs | 36 +- osu.Game/OsuGameBase.cs | 3 + osu.Game/app.config | 15 + osu.Game/osu.Game.csproj | 9 + osu.Game/packages.config | 1 + 15 files changed, 536 insertions(+), 328 deletions(-) create mode 100644 osu.Game/Database/ScoreDatabase.cs create mode 100644 osu.Game/IPC/ScoreIPCChannel.cs create mode 100644 osu.Game/Modes/LegacyReplay.cs create mode 100644 osu.Game/Modes/Replay.cs create mode 100644 osu.Game/app.config diff --git a/osu-framework b/osu-framework index 798409058a..7193ee4f91 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 798409058a421307b5a92aeea4cd60a065f5a0d4 +Subproject commit 7193ee4f91e8cec88e872fc4950748138b0c87a6 diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs index fe69469db7..812d0b6cfd 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs @@ -26,88 +26,13 @@ using osu.Game.Screens.Play; using OpenTK.Graphics; using OpenTK.Input; using SharpCompress.Archives.SevenZip; +using SharpCompress.Compressors.LZMA; using SharpCompress.Readers; using KeyboardState = osu.Framework.Input.KeyboardState; using MouseState = osu.Framework.Input.MouseState; namespace osu.Desktop.VisualTests.Tests { - public class ScoreDatabase - { - private readonly Storage storage; - - private const string replay_folder = @"replays"; - - public ScoreDatabase(Storage storage) - { - this.storage = storage; - } - - public Score ReadReplayFile(string replayFilename) - { - Score score; - - using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename))) - using (SerializationReader sr = new SerializationReader(s)) - { - var ruleset = Ruleset.GetRuleset((PlayMode)sr.ReadByte()); - var processor = ruleset.CreateScoreProcessor(); - - score = processor.GetScore(); - - - - /* score.Pass = true;*/ - var version = sr.ReadInt32(); - /* score.FileChecksum = */ sr.ReadString(); - /* score.PlayerName = */ 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(); - - //new ASCIIEncoding().GetString(SevenZipHelper.Decompress(ReplayCompressed))); - - score.Replay = new LegacyReplay(); - - //float lastTime = 0; - //foreach (var l in File.ReadAllText(@"C:\Users\Dean\Desktop\2157025197").Split(',')) - //{ - // var split = l.Split('|'); - - // if (split.Length < 4 || float.Parse(split[0]) < 0) continue; - - // lastTime += float.Parse(split[0]); - - // list.Add(new LegacyReplay.LegacyReplayInputHandler.LegacyReplayFrame( - // lastTime, - // float.Parse(split[1]), - // 384 - float.Parse(split[2]), - // (LegacyReplay.LegacyReplayInputHandler.LegacyButtonState)int.Parse(split[3]) - // )); - //} - } - - return score; - } - } - class TestCaseReplay : TestCasePlayer { private WorkingBeatmap beatmap; @@ -134,235 +59,4 @@ namespace osu.Desktop.VisualTests.Tests return player; } } - - public class LegacyReplay : Replay - { - private new List frames = new List(); - - public override ReplayInputHandler GetInputHandler() => new LegacyReplayInputHandler(frames); - - /// - /// 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 : ReplayInputHandler - { - 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 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 override 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}"; - } - } - } } diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index d3358da013..9f25ed4efd 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -9,6 +9,7 @@ using osu.Framework.Desktop.Platform; using osu.Desktop.Overlays; using System.Reflection; using System.Drawing; +using System.IO; using System.Threading.Tasks; using osu.Game.Screens.Menu; @@ -55,19 +56,29 @@ namespace osu.Desktop private void dragDrop(DragEventArgs e) { // this method will only be executed if e.Effect in dragEnter gets set to something other that None. - var dropData = e.Data.GetData(DataFormats.FileDrop) as object[]; + var dropData = (object[])e.Data.GetData(DataFormats.FileDrop); var filePaths = dropData.Select(f => f.ToString()).ToArray(); - Task.Run(() => BeatmapDatabase.Import(filePaths)); + + if (filePaths.All(f => Path.GetExtension(f) == @".osz")) + Task.Run(() => BeatmapDatabase.Import(filePaths)); + else if (filePaths.All(f => Path.GetExtension(f) == @".osr")) + Task.Run(() => + { + var score = ScoreDatabase.ReadReplayFile(filePaths.First()); + Schedule(() => LoadScore(score)); + }); } + static readonly string[] allowed_extensions = { @".osz", @".osr" }; + private void dragEnter(DragEventArgs e) { // dragDrop will only be executed if e.Effect gets set to something other that None in this method. bool isFile = e.Data.GetDataPresent(DataFormats.FileDrop); if (isFile) { - var paths = (e.Data.GetData(DataFormats.FileDrop) as object[]).Select(f => f.ToString()).ToArray(); - if (paths.Any(p => !p.EndsWith(".osz"))) + var paths = ((object[])e.Data.GetData(DataFormats.FileDrop)).Select(f => f.ToString()).ToArray(); + if (paths.Any(p => !allowed_extensions.Any(ext => p.EndsWith(ext)))) e.Effect = DragDropEffects.None; else e.Effect = DragDropEffects.Copy; diff --git a/osu.Game/Database/BeatmapDatabase.cs b/osu.Game/Database/BeatmapDatabase.cs index df8ead7f94..0d8d1a676c 100644 --- a/osu.Game/Database/BeatmapDatabase.cs +++ b/osu.Game/Database/BeatmapDatabase.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Linq.Expressions; using System.Security.Cryptography; +using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -162,7 +163,7 @@ namespace osu.Game.Database /// Location on disk public void Import(string path) { - Import(new [] { path }); + Import(new[] { path }); } /// @@ -181,10 +182,9 @@ namespace osu.Game.Database if (File.Exists(path)) // Not always the case, i.e. for LegacyFilesystemReader { - using (var md5 = MD5.Create()) using (var input = storage.GetStream(path)) { - hash = BitConverter.ToString(md5.ComputeHash(input)).Replace("-", "").ToLowerInvariant(); + hash = input.GetMd5Hash(); input.Seek(0, SeekOrigin.Begin); path = Path.Combine(@"beatmaps", hash.Remove(1), hash.Remove(2), hash); if (!storage.Exists(path)) @@ -216,22 +216,29 @@ namespace osu.Game.Database Metadata = metadata }; - using (var reader = ArchiveReader.GetReader(storage, path)) + using (var archive = ArchiveReader.GetReader(storage, path)) { - string[] mapNames = reader.BeatmapFilenames; + string[] mapNames = archive.BeatmapFilenames; foreach (var name in mapNames) - using (var stream = new StreamReader(reader.GetStream(name))) + using (var raw = archive.GetStream(name)) + using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit + using (var sr = new StreamReader(ms)) { - var decoder = BeatmapDecoder.GetDecoder(stream); - Beatmap beatmap = decoder.Decode(stream); + raw.CopyTo(ms); + ms.Position = 0; + + var decoder = BeatmapDecoder.GetDecoder(sr); + Beatmap beatmap = decoder.Decode(sr); + beatmap.BeatmapInfo.Path = name; + beatmap.BeatmapInfo.Hash = ms.GetMd5Hash(); // TODO: Diff beatmap metadata with set metadata and leave it here if necessary beatmap.BeatmapInfo.Metadata = null; beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); } - beatmapSet.StoryboardFile = reader.StoryboardFilename; + beatmapSet.StoryboardFile = archive.StoryboardFilename; } return beatmapSet; diff --git a/osu.Game/Database/BeatmapInfo.cs b/osu.Game/Database/BeatmapInfo.cs index 1c2ae2bf78..3d9aa1092b 100644 --- a/osu.Game/Database/BeatmapInfo.cs +++ b/osu.Game/Database/BeatmapInfo.cs @@ -39,6 +39,8 @@ namespace osu.Game.Database public string Path { get; set; } + public string Hash { get; set; } + // General public int AudioLeadIn { get; set; } public bool Countdown { get; set; } @@ -64,6 +66,7 @@ namespace osu.Game.Database StoredBookmarks = string.Join(",", value); } } + public double DistanceSpacing { get; set; } public int BeatDivisor { get; set; } public int GridSize { get; set; } diff --git a/osu.Game/Database/ScoreDatabase.cs b/osu.Game/Database/ScoreDatabase.cs new file mode 100644 index 0000000000..6fc65331ff --- /dev/null +++ b/osu.Game/Database/ScoreDatabase.cs @@ -0,0 +1,110 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.IO; +using osu.Framework.Platform; +using osu.Game.IO.Legacy; +using osu.Game.IPC; +using osu.Game.Modes; +using SharpCompress.Compressors.LZMA; + +namespace osu.Game.Database +{ + public class ScoreDatabase + { + private readonly Storage storage; + private readonly BeatmapDatabase beatmaps; + + private const string replay_folder = @"replays"; + + private ScoreIPCChannel ipc; + + public ScoreDatabase(Storage storage, IIpcHost importHost = null, BeatmapDatabase beatmaps = null) + { + this.storage = storage; + this.beatmaps = beatmaps; + + if (importHost != null) + ipc = new ScoreIPCChannel(importHost, this); + } + + public Score ReadReplayFile(string replayFilename) + { + Score score; + + using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename))) + using (SerializationReader sr = new SerializationReader(s)) + { + var ruleset = Ruleset.GetRuleset((PlayMode)sr.ReadByte()); + var processor = ruleset.CreateScoreProcessor(); + + score = processor.GetScore(); + + /* score.Pass = true;*/ + var version = sr.ReadInt32(); + /* score.FileChecksum = */ + var beatmapHash = sr.ReadString(); + score.Beatmap = beatmaps.Query().Where(b => b.Hash == beatmapHash).FirstOrDefault(); + /* score.PlayerName = */ + 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 Exception("input .lzma is too short")); + long outSize = 0; + for (int i = 0; i < 8; i++) + { + int v = replayInStream.ReadByte(); + if (v < 0) + throw (new Exception("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 LegacyReplay(reader); + } + } + + return score; + } + } +} diff --git a/osu.Game/IPC/ScoreIPCChannel.cs b/osu.Game/IPC/ScoreIPCChannel.cs new file mode 100644 index 0000000000..289f5454e6 --- /dev/null +++ b/osu.Game/IPC/ScoreIPCChannel.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Diagnostics; +using System.Threading.Tasks; +using osu.Framework.Platform; +using osu.Game.Database; + +namespace osu.Game.IPC +{ + public class ScoreIPCChannel : IpcChannel + { + private ScoreDatabase scores; + + public ScoreIPCChannel(IIpcHost host, ScoreDatabase scores = null) + : base(host) + { + this.scores = scores; + MessageReceived += (msg) => + { + Debug.Assert(scores != null); + ImportAsync(msg.Path); + }; + } + + public async Task ImportAsync(string path) + { + if (scores == null) + { + //we want to contact a remote osu! to handle the import. + await SendMessageAsync(new ScoreImportMessage { Path = path }); + return; + } + + scores.ReadReplayFile(path); + } + } + + public class ScoreImportMessage + { + public string Path; + } +} diff --git a/osu.Game/Modes/LegacyReplay.cs b/osu.Game/Modes/LegacyReplay.cs new file mode 100644 index 0000000000..cda9bf0de3 --- /dev/null +++ b/osu.Game/Modes/LegacyReplay.cs @@ -0,0 +1,269 @@ +// 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; +using osu.Framework.MathUtils; +using osu.Game.Input.Handlers; +using osu.Game.IO.Legacy; +using OpenTK; +using OpenTK.Input; +using KeyboardState = osu.Framework.Input.KeyboardState; +using MouseState = osu.Framework.Input.MouseState; + +namespace osu.Game.Modes +{ + public class LegacyReplay : Replay + { + private List frames = new List(); + + public LegacyReplay(StreamReader reader) + { + float lastTime = 0; + + foreach (var l in reader.ReadToEnd().Split(',')) + { + var split = l.Split('|'); + + if (split.Length < 4 || float.Parse(split[0]) < 0) continue; + + lastTime += float.Parse(split[0]); + + frames.Add(new LegacyReplayFrame( + lastTime, + float.Parse(split[1]), + 384 - float.Parse(split[2]), + (LegacyButtonState)int.Parse(split[3]) + )); + } + } + + public override ReplayInputHandler GetInputHandler() => new LegacyReplayInputHandler(frames); + + /// + /// 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 : ReplayInputHandler + { + 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 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 override 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}"; + } + } + } +} diff --git a/osu.Game/Modes/Replay.cs b/osu.Game/Modes/Replay.cs new file mode 100644 index 0000000000..0a41a12335 --- /dev/null +++ b/osu.Game/Modes/Replay.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Input.Handlers; + +namespace osu.Game.Modes +{ + public abstract class Replay + { + public virtual ReplayInputHandler GetInputHandler() => null; + } +} \ No newline at end of file diff --git a/osu.Game/Modes/Score.cs b/osu.Game/Modes/Score.cs index a1324f614f..64dda4e181 100644 --- a/osu.Game/Modes/Score.cs +++ b/osu.Game/Modes/Score.cs @@ -2,7 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Input.Handlers; -using osu.Game.Input.Handlers; +using osu.Game.Database; +using SQLite.Net; namespace osu.Game.Modes { @@ -15,10 +16,6 @@ namespace osu.Game.Modes public double Health { get; set; } public Replay Replay; - } - - public class Replay - { - public virtual ReplayInputHandler GetInputHandler() => null; + public BeatmapInfo Beatmap; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c56907c86d..8cd7dd172c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -24,9 +24,10 @@ using osu.Game.Screens.Menu; using OpenTK; using System.Linq; using osu.Framework.Graphics.Primitives; -using System.Collections.Generic; using System.Threading.Tasks; +using osu.Game.Graphics; using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Play; namespace osu.Game { @@ -92,6 +93,39 @@ namespace osu.Game PlayMode = LocalConfig.GetBindable(OsuConfig.PlayMode); } + protected void LoadScore(Score s) + { + var menu = intro.ChildScreen; + + if (menu == null) + { + Schedule(() => LoadScore(s)); + return; + } + + if (!menu.IsCurrentScreen) + { + menu.MakeCurrent(); + Delay(500); + Schedule(() => LoadScore(s)); + return; + } + + if (s.Beatmap == null) + { + notificationManager.Post(new SimpleNotification + { + Text = @"Tried to load a score for a beatmap we don't have!", + Icon = FontAwesome.fa_life_saver, + }); + return; + } + + Beatmap.Value = BeatmapDatabase.GetWorkingBeatmap(s.Beatmap); + + menu.Push(new PlayerLoader(new Player { ReplayInputHandler = s.Replay.GetInputHandler() })); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1c34743567..7730fa263c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -25,6 +25,8 @@ namespace osu.Game protected BeatmapDatabase BeatmapDatabase; + protected ScoreDatabase ScoreDatabase; + protected override string MainResourceFile => @"osu.Game.Resources.dll"; public APIAccess API; @@ -43,6 +45,7 @@ namespace osu.Game Dependencies.Cache(this); Dependencies.Cache(LocalConfig); Dependencies.Cache(BeatmapDatabase = new BeatmapDatabase(Host.Storage, Host)); + Dependencies.Cache(ScoreDatabase = new ScoreDatabase(Host.Storage, Host, BeatmapDatabase)); Dependencies.Cache(new OsuColour()); //this completely overrides the framework default. will need to change once we make a proper FontStore. diff --git a/osu.Game/app.config b/osu.Game/app.config new file mode 100644 index 0000000000..b9af3fdc80 --- /dev/null +++ b/osu.Game/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index f5c9b8723e..139506b3a1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -43,6 +43,10 @@ $(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1340\lib\net45\OpenTK.dll True + + ..\packages\SharpCompress.0.15.1\lib\net45\SharpCompress.dll + True + $(SolutionDir)\packages\SQLite.Net.Core-PCL.3.1.1\lib\portable-win8+net45+wp8+wpa81+MonoAndroid1+MonoTouch1\SQLite.Net.dll True @@ -69,6 +73,7 @@ + @@ -83,9 +88,12 @@ + + + @@ -329,6 +337,7 @@ osu.licenseheader + diff --git a/osu.Game/packages.config b/osu.Game/packages.config index 15d28ca24f..93df6b7e42 100644 --- a/osu.Game/packages.config +++ b/osu.Game/packages.config @@ -7,6 +7,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste + From a5d044067cdeba17c54e701428d6795f38a692dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Mar 2017 21:35:12 +0900 Subject: [PATCH 10/34] Cancel previous load attempts before starting a new score load. --- osu.Game/OsuGame.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8cd7dd172c..79be419aa6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -25,6 +25,7 @@ using OpenTK; using System.Linq; using osu.Framework.Graphics.Primitives; using System.Threading.Tasks; +using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Play; @@ -93,13 +94,17 @@ namespace osu.Game PlayMode = LocalConfig.GetBindable(OsuConfig.PlayMode); } + private ScheduledDelegate scoreLoad; + protected void LoadScore(Score s) { + scoreLoad?.Cancel(); + var menu = intro.ChildScreen; if (menu == null) { - Schedule(() => LoadScore(s)); + scoreLoad = Schedule(() => LoadScore(s)); return; } @@ -107,7 +112,7 @@ namespace osu.Game { menu.MakeCurrent(); Delay(500); - Schedule(() => LoadScore(s)); + scoreLoad = Schedule(() => LoadScore(s)); return; } From 7a6a614358aea1b1085e22fdcd9d9b8be6dfc5a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Mar 2017 21:35:26 +0900 Subject: [PATCH 11/34] Don't show pause menu when watching replays. --- osu.Game/Screens/Play/Player.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e3651f0869..51f8781fd6 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -315,6 +315,8 @@ namespace osu.Game.Screens.Play { if (pauseOverlay == null) return false; + if (ReplayInputHandler != null) return false; + if (pauseOverlay.State != Visibility.Visible && !canPause) return true; if (!IsPaused && sourceClock.IsRunning) // For if the user presses escape quickly when entering the map From 7afcac366097926c66ef808d057cff4c8b9eda23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 5 Mar 2017 17:45:40 +0900 Subject: [PATCH 12/34] Move PreferredPlayMode to WorkingBeatmap. --- osu.Desktop.VisualTests/Tests/TestCasePlayer.cs | 1 - osu.Game/Beatmaps/WorkingBeatmap.cs | 9 +++++++++ osu.Game/Screens/Play/Player.cs | 6 +----- osu.Game/Screens/Select/PlaySongSelect.cs | 5 +++-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs index 2dd6c00da2..009680a978 100644 --- a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs +++ b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs @@ -97,7 +97,6 @@ namespace osu.Desktop.VisualTests.Tests { return new Player { - PreferredPlayMode = PlayMode.Osu, Beatmap = beatmap }; } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 9393c2b0e9..0ee45d871b 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.IO; using osu.Game.Database; +using osu.Game.Modes; namespace osu.Game.Beatmaps { @@ -17,6 +18,14 @@ namespace osu.Game.Beatmaps public readonly BeatmapSetInfo BeatmapSetInfo; + /// + /// A play mode that is preferred for this beatmap. This allows for conversion between game modes where feasible, + /// but does not gurantee an outcome. + /// + public PlayMode PreferredPlayMode; + + public PlayMode PlayMode => beatmap?.BeatmapInfo?.Mode > PlayMode.Osu ? beatmap.BeatmapInfo.Mode : PreferredPlayMode; + public readonly bool WithStoryboard; protected abstract ArchiveReader GetReader(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 51f8781fd6..4cd919874e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -42,8 +42,6 @@ namespace osu.Game.Screens.Play public BeatmapInfo BeatmapInfo; - public PlayMode PreferredPlayMode; - private bool isPaused; public bool IsPaused { @@ -121,9 +119,7 @@ namespace osu.Game.Screens.Play return; } - PlayMode usablePlayMode = beatmap.BeatmapInfo?.Mode > PlayMode.Osu ? beatmap.BeatmapInfo.Mode : PreferredPlayMode; - - ruleset = Ruleset.GetRuleset(usablePlayMode); + ruleset = Ruleset.GetRuleset(Beatmap.PlayMode); scoreOverlay = ruleset.CreateScoreOverlay(); scoreOverlay.BindProcessor(scoreProcessor = ruleset.CreateScoreProcessor(beatmap.HitObjects.Count)); diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 316cc50dae..f35f39a452 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -159,10 +159,11 @@ namespace osu.Game.Screens.Select if (player != null || Beatmap == null) return; + Beatmap.PreferredPlayMode = playMode.Value; + (player = new PlayerLoader(new Player { - BeatmapInfo = carousel.SelectedGroup.SelectedPanel.Beatmap, - PreferredPlayMode = playMode.Value + Beatmap = Beatmap, //eagerly set this so it's prsent before push. })).LoadAsync(Game, l => Push(player)); } }, From 1c5b918f9e0bdad52d2d602f659bff08d56586ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 5 Mar 2017 17:46:00 +0900 Subject: [PATCH 13/34] Add osu! autoplay generation. Doesn't work on complex sliders yet. --- .../Tests/TestCaseReplay.cs | 2 +- .../Objects/Drawables/DrawableHitCircle.cs | 19 +- .../Objects/Drawables/Pieces/CirclePiece.cs | 2 +- osu.Game.Modes.Osu/OsuAutoReplay.cs | 312 ++++++++++++++++++ osu.Game.Modes.Osu/OsuRuleset.cs | 13 +- osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj | 1 + osu.Game/Modes/LegacyReplay.cs | 11 +- osu.Game/Modes/Ruleset.cs | 3 + 8 files changed, 349 insertions(+), 14 deletions(-) create mode 100644 osu.Game.Modes.Osu/OsuAutoReplay.cs diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs index 90a4a78725..2bb77a8a7c 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs @@ -53,7 +53,7 @@ namespace osu.Desktop.VisualTests.Tests protected override Player CreatePlayer(WorkingBeatmap beatmap) { var player = base.CreatePlayer(beatmap); - player.ReplayInputHandler = scoreDatabase.ReadReplayFile(@"Tao - O2i3 - Ooi [Game Edit] [Advanced] (2016-08-08) Osu.osr").Replay.GetInputHandler(); + player.ReplayInputHandler = Ruleset.GetRuleset(beatmap.PlayMode).CreateAutoplayReplay(beatmap.Beatmap)?.Replay?.GetInputHandler(); return player; } } diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs index 9197472f92..6c05cb0b17 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -69,15 +69,18 @@ namespace osu.Game.Modes.Osu.Objects.Drawables Size = circle.DrawSize; } - double hit50 = 150; - double hit100 = 80; - double hit300 = 30; + //todo: these aren't constants. + public const double HITTABLE_RANGE = 300; + public const double HIT_WINDOW_50 = 150; + public const double HIT_WINDOW_100 = 80; + public const double HIT_WINDOW_300 = 30; + public const double CIRCLE_RADIUS = 64; protected override void CheckJudgement(bool userTriggered) { if (!userTriggered) { - if (Judgement.TimeOffset > hit50) + if (Judgement.TimeOffset > HIT_WINDOW_50) Judgement.Result = HitResult.Miss; return; } @@ -86,15 +89,15 @@ namespace osu.Game.Modes.Osu.Objects.Drawables OsuJudgementInfo osuJudgement = Judgement as OsuJudgementInfo; - if (hitOffset < hit50) + if (hitOffset < HIT_WINDOW_50) { Judgement.Result = HitResult.Hit; - if (hitOffset < hit300) + if (hitOffset < HIT_WINDOW_300) osuJudgement.Score = OsuScoreResult.Hit300; - else if (hitOffset < hit100) + else if (hitOffset < HIT_WINDOW_100) osuJudgement.Score = OsuScoreResult.Hit100; - else if (hitOffset < hit50) + else if (hitOffset < HIT_WINDOW_50) osuJudgement.Score = OsuScoreResult.Hit50; } else diff --git a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/CirclePiece.cs index 23f878d491..93974d1969 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -21,7 +21,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables.Pieces public CirclePiece() { - Size = new Vector2(128); + Size = new Vector2((float)DrawableHitCircle.CIRCLE_RADIUS * 2); Masking = true; CornerRadius = Size.X / 2; diff --git a/osu.Game.Modes.Osu/OsuAutoReplay.cs b/osu.Game.Modes.Osu/OsuAutoReplay.cs new file mode 100644 index 0000000000..eb6e208059 --- /dev/null +++ b/osu.Game.Modes.Osu/OsuAutoReplay.cs @@ -0,0 +1,312 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Modes.Osu.Objects; +using OpenTK; +using System; +using osu.Framework.Graphics.Transforms; +using osu.Game.Modes.Osu.Objects.Drawables; +using osu.Framework.MathUtils; +using System.Diagnostics; + +namespace osu.Game.Modes.Osu +{ + public class OsuAutoReplay : LegacyReplay + { + private Beatmap beatmap; + + public OsuAutoReplay(Beatmap beatmap) + { + this.beatmap = beatmap; + + createAutoReplay(); + } + + internal class LegacyReplayFrameComparer : IComparer + { + public int Compare(LegacyReplayFrame f1, LegacyReplayFrame f2) + { + return f1.Time.CompareTo(f2.Time); + } + } + + private static IComparer replayFrameComparer = new LegacyReplayFrameComparer(); + + private static int FindInsertionIndex(List replay, LegacyReplayFrame frame) + { + int index = replay.BinarySearch(frame, replayFrameComparer); + + if (index < 0) + { + index = ~index; + } + else + { + // Go to the first index which is actually bigger + while (index < replay.Count && frame.Time == replay[index].Time) + { + ++index; + } + } + + return index; + } + + private static void AddFrameToReplay(List replay, LegacyReplayFrame frame) + { + replay.Insert(FindInsertionIndex(replay, frame), frame); + } + + private static Vector2 CirclePosition(double t, double radius) + { + return new Vector2((float)(Math.Cos(t) * radius), (float)(Math.Sin(t) * radius)); + } + + private void createAutoReplay() + { + int buttonIndex = 0; + + bool delayedMovements = false;// ModManager.CheckActive(Mods.Relax2); + EasingTypes preferredEasing = delayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out; + + AddFrameToReplay(Frames, new LegacyReplayFrame(-100000, 256, 500, LegacyButtonState.None)); + AddFrameToReplay(Frames, new LegacyReplayFrame(beatmap.HitObjects[0].StartTime - 1500, 256, 500, LegacyButtonState.None)); + AddFrameToReplay(Frames, new LegacyReplayFrame(beatmap.HitObjects[0].StartTime - 1000, 256, 192, LegacyButtonState.None)); + + // We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps. + float frameDelay = (float)applyModsToRate(1000.0 / 60.0); + Vector2 spinnerCentre = new Vector2(256, 192); + const float spinnerRadius = 50; + + // Already superhuman, but still somewhat realistic + int reactionTime = (int)applyModsToRate(100); + + + for (int i = 0; i < beatmap.HitObjects.Count; i++) + { + OsuHitObject h = beatmap.HitObjects[i] as OsuHitObject; + + //if (h.EndTime < InputManager.ReplayStartTime) + //{ + // h.IsHit = true; + // continue; + //} + + int endDelay = h is Spinner ? 1 : 0; + + if (delayedMovements && i > 0) + { + OsuHitObject last = beatmap.HitObjects[i - 1] as OsuHitObject; + + //Make the cursor stay at a hitObject as long as possible (mainly for autopilot). + if (h.StartTime - DrawableHitCircle.HITTABLE_RANGE > last.EndTime + DrawableHitCircle.HIT_WINDOW_50 + 50) + { + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) AddFrameToReplay(Frames, new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) AddFrameToReplay(Frames, new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HITTABLE_RANGE, h.Position.X, h.Position.Y, LegacyButtonState.None)); + } + else if (h.StartTime - DrawableHitCircle.HIT_WINDOW_50 > last.EndTime + DrawableHitCircle.HIT_WINDOW_50 + 50) + { + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) AddFrameToReplay(Frames, new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) AddFrameToReplay(Frames, new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HIT_WINDOW_50, h.Position.X, h.Position.Y, LegacyButtonState.None)); + } + else if (h.StartTime - DrawableHitCircle.HIT_WINDOW_100 > last.EndTime + DrawableHitCircle.HIT_WINDOW_100 + 50) + { + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) AddFrameToReplay(Frames, new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_100, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) AddFrameToReplay(Frames, new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HIT_WINDOW_100, h.Position.X, h.Position.Y, LegacyButtonState.None)); + } + } + + + Vector2 targetPosition = h.Position; + EasingTypes easing = preferredEasing; + float spinnerDirection = -1; + + if (h is Spinner) + { + targetPosition.X = Frames[Frames.Count - 1].MouseX; + targetPosition.Y = Frames[Frames.Count - 1].MouseY; + + Vector2 difference = spinnerCentre - targetPosition; + + float differenceLength = difference.Length; + float newLength = (float)Math.Sqrt(differenceLength * differenceLength - spinnerRadius * spinnerRadius); + + if (differenceLength > spinnerRadius) + { + float angle = (float)Math.Asin(spinnerRadius / differenceLength); + + if (angle > 0) + { + spinnerDirection = -1; + } + else + { + spinnerDirection = 1; + } + + difference.X = difference.X * (float)Math.Cos(angle) - difference.Y * (float)Math.Sin(angle); + difference.Y = difference.X * (float)Math.Sin(angle) + difference.Y * (float)Math.Cos(angle); + + difference.Normalize(); + difference *= newLength; + + targetPosition += difference; + + easing = EasingTypes.In; + } + else if (difference.Length > 0) + { + targetPosition = spinnerCentre - difference * (spinnerRadius / difference.Length); + } + else + { + targetPosition = spinnerCentre + new Vector2(0, -spinnerRadius); + } + } + + + // Do some nice easing for cursor movements + if (Frames.Count > 0) + { + LegacyReplayFrame lastFrame = Frames[Frames.Count - 1]; + + // Wait until Auto could "see and react" to the next note. + double waitTime = h.StartTime - Math.Max(0.0, DrawableOsuHitObject.TIME_PREEMPT - reactionTime); + if (waitTime > lastFrame.Time) + { + lastFrame = new LegacyReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState); + AddFrameToReplay(Frames, lastFrame); + } + + Vector2 lastPosition = new Vector2(lastFrame.MouseX, lastFrame.MouseY); + + double timeDifference = applyModsToTime(h.StartTime - lastFrame.Time); + + // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. + if (timeDifference > 0 && // Sanity checks + ((lastPosition - targetPosition).Length > DrawableHitCircle.CIRCLE_RADIUS * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough + timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. + { + // Perform eased movement + for (double time = lastFrame.Time + frameDelay; time < h.StartTime; time += frameDelay) + { + Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPosition, lastFrame.Time, h.StartTime, easing); + AddFrameToReplay(Frames, new LegacyReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState)); + } + + buttonIndex = 0; + } + else + { + buttonIndex++; + } + } + + LegacyButtonState button = buttonIndex % 2 == 0 ? LegacyButtonState.Left1 : LegacyButtonState.Right1; + LegacyButtonState previousButton = LegacyButtonState.None; + + LegacyReplayFrame newFrame = new LegacyReplayFrame(h.StartTime, targetPosition.X, targetPosition.Y, button); + LegacyReplayFrame endFrame = new LegacyReplayFrame(h.EndTime + endDelay, h.EndPosition.X, h.EndPosition.Y, LegacyButtonState.None); + + // Decrement because we want the previous frame, not the next one + int index = FindInsertionIndex(Frames, newFrame) - 1; + + // Do we have a previous frame? No need to check for < replay.Count since we decremented! + if (index >= 0) + { + LegacyReplayFrame previousFrame = Frames[index]; + previousButton = previousFrame.ButtonState; + + // If a button is already held, then we simply alternate + if (previousButton != LegacyButtonState.None) + { + Debug.Assert(previousButton != (LegacyButtonState.Left1 | LegacyButtonState.Right1)); + + // Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button. + if (previousButton == button) + { + button = (LegacyButtonState.Left1 | LegacyButtonState.Right1) & ~button; + newFrame.SetButtonStates(button); + } + + // We always follow the most recent slider / spinner, so remove any other frames that occur while it exists. + int endIndex = FindInsertionIndex(Frames, endFrame); + + if (index < Frames.Count - 1) + Frames.RemoveRange(index + 1, Math.Max(0, endIndex - (index + 1))); + + // After alternating we need to keep holding the other button in the future rather than the previous one. + for (int j = index + 1; j < Frames.Count; ++j) + { + // Don't affect frames which stop pressing a button! + if (j < Frames.Count - 1 || Frames[j].ButtonState == previousButton) + Frames[j].SetButtonStates(button); + } + } + } + + AddFrameToReplay(Frames, newFrame); + + // We add intermediate frames for spinning / following a slider here. + if (h is Spinner) + { + Vector2 difference = targetPosition - spinnerCentre; + + float radius = difference.Length; + float angle = radius == 0 ? 0 : (float)Math.Atan2(difference.Y, difference.X); + + double t; + + for (double j = h.StartTime + frameDelay; j < h.EndTime; j += frameDelay) + { + t = applyModsToTime(j - h.StartTime) * spinnerDirection; + + Vector2 pos = spinnerCentre + CirclePosition(t / 20 + angle, spinnerRadius); + AddFrameToReplay(Frames, new LegacyReplayFrame((int)j, pos.X, pos.Y, button)); + } + + t = applyModsToTime(h.EndTime - h.StartTime) * spinnerDirection; + Vector2 endPosition = spinnerCentre + CirclePosition(t / 20 + angle, spinnerRadius); + + AddFrameToReplay(Frames, new LegacyReplayFrame(h.EndTime, endPosition.X, endPosition.Y, button)); + + endFrame.MouseX = endPosition.X; + endFrame.MouseY = endPosition.Y; + } + else if (h is Slider) + { + Slider s = h as Slider; + int lastTime = 0; + + //foreach ( + // Transformation t in + // s..Transformations.FindAll( + // tr => tr.Type == TransformationType.Movement)) + //{ + // if (lastTime != 0 && t.Time1 - lastTime < frameDelay) continue; + + // AddFrameToReplay(Frames, new LegacyReplayFrame(t.Time1, t.StartVector.X, t.StartVector.Y, + // button)); + // lastTime = t.Time1; + //} + + AddFrameToReplay(Frames, new LegacyReplayFrame(h.EndTime, s.EndPosition.X, s.EndPosition.Y, button)); + } + + // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! + if (Frames[Frames.Count - 1].Time <= endFrame.Time) + { + AddFrameToReplay(Frames, endFrame); + } + } + + //Player.currentScore.Replay = InputManager.ReplayScore.Replay; + //Player.currentScore.PlayerName = "osu!"; + } + + private double applyModsToTime(double v) => v; + private double applyModsToRate(double v) => v; + } +} diff --git a/osu.Game.Modes.Osu/OsuRuleset.cs b/osu.Game.Modes.Osu/OsuRuleset.cs index 33cbeafc03..80b873db3a 100644 --- a/osu.Game.Modes.Osu/OsuRuleset.cs +++ b/osu.Game.Modes.Osu/OsuRuleset.cs @@ -101,10 +101,21 @@ namespace osu.Game.Modes.Osu public override HitObjectParser CreateHitObjectParser() => new OsuHitObjectParser(); - public override ScoreProcessor CreateScoreProcessor(int hitObjectCount) => new OsuScoreProcessor(hitObjectCount); + public override ScoreProcessor CreateScoreProcessor(int hitObjectCount = 0) => new OsuScoreProcessor(hitObjectCount); public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new OsuDifficultyCalculator(beatmap); + public override Score CreateAutoplayReplay(Beatmap beatmap) + { + var processor = CreateScoreProcessor(); + + var score = processor.GetScore(); + + score.Replay = new OsuAutoReplay(beatmap); + + return score; + } + protected override PlayMode PlayMode => PlayMode.Osu; } } diff --git a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj index f53066ecde..0571ec2956 100644 --- a/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj +++ b/osu.Game.Modes.Osu/osu.Game.Modes.Osu.csproj @@ -70,6 +70,7 @@ + diff --git a/osu.Game/Modes/LegacyReplay.cs b/osu.Game/Modes/LegacyReplay.cs index cda9bf0de3..0ed63b309e 100644 --- a/osu.Game/Modes/LegacyReplay.cs +++ b/osu.Game/Modes/LegacyReplay.cs @@ -17,7 +17,12 @@ namespace osu.Game.Modes { public class LegacyReplay : Replay { - private List frames = new List(); + protected List Frames = new List(); + + protected LegacyReplay() + { + + } public LegacyReplay(StreamReader reader) { @@ -31,7 +36,7 @@ namespace osu.Game.Modes lastTime += float.Parse(split[0]); - frames.Add(new LegacyReplayFrame( + Frames.Add(new LegacyReplayFrame( lastTime, float.Parse(split[1]), 384 - float.Parse(split[2]), @@ -40,7 +45,7 @@ namespace osu.Game.Modes } } - public override ReplayInputHandler GetInputHandler() => new LegacyReplayInputHandler(frames); + public override ReplayInputHandler GetInputHandler() => new LegacyReplayInputHandler(Frames); /// /// The ReplayHandler will take a replay and handle the propagation of updates to the input stack. diff --git a/osu.Game/Modes/Ruleset.cs b/osu.Game/Modes/Ruleset.cs index 597ee949b4..2627f3faf0 100644 --- a/osu.Game/Modes/Ruleset.cs +++ b/osu.Game/Modes/Ruleset.cs @@ -43,6 +43,8 @@ namespace osu.Game.Modes public virtual FontAwesome Icon => FontAwesome.fa_question_circle; + public virtual Score CreateAutoplayReplay(Beatmap beatmap) => null; + public static Ruleset GetRuleset(PlayMode mode) { Type type; @@ -52,5 +54,6 @@ namespace osu.Game.Modes return Activator.CreateInstance(type) as Ruleset; } + } } From 54945415c01db172ce8dc4afe03ac43035487cc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 10:05:42 +0900 Subject: [PATCH 14/34] Remove unnecessary usings. --- .../Tests/TestCaseReplay.cs | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs index 2bb77a8a7c..2a36428f74 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs @@ -2,34 +2,14 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Generic; using System.IO; -using System.Text; using osu.Framework.Allocation; -using osu.Framework.Screens.Testing; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; -using OpenTK; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; using osu.Framework.Input.Handlers; -using osu.Framework.MathUtils; using osu.Framework.Platform; -using osu.Game.Beatmaps.IO; +using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Input.Handlers; -using osu.Game.IO.Legacy; using osu.Game.Modes; -using osu.Game.Modes.Objects; -using osu.Game.Modes.Osu.Objects; using osu.Game.Screens.Play; -using OpenTK.Graphics; -using OpenTK.Input; -using SharpCompress.Archives.SevenZip; -using SharpCompress.Compressors.LZMA; -using SharpCompress.Readers; -using KeyboardState = osu.Framework.Input.KeyboardState; -using MouseState = osu.Framework.Input.MouseState; namespace osu.Desktop.VisualTests.Tests { From 5b4424d4fab18b5abe1381c4dbde5a9c07facf12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 10:06:14 +0900 Subject: [PATCH 15/34] CreateAutoplayReplay -> CreateAutoplayScore. --- osu.Desktop.VisualTests/Tests/TestCaseReplay.cs | 2 +- osu.Game.Modes.Osu/OsuRuleset.cs | 8 ++------ osu.Game/Modes/Ruleset.cs | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs index 2a36428f74..64b33a78c9 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs @@ -33,7 +33,7 @@ namespace osu.Desktop.VisualTests.Tests protected override Player CreatePlayer(WorkingBeatmap beatmap) { var player = base.CreatePlayer(beatmap); - player.ReplayInputHandler = Ruleset.GetRuleset(beatmap.PlayMode).CreateAutoplayReplay(beatmap.Beatmap)?.Replay?.GetInputHandler(); + player.ReplayInputHandler = Ruleset.GetRuleset(beatmap.PlayMode).CreateAutoplayScore(beatmap.Beatmap)?.Replay?.GetInputHandler(); return player; } } diff --git a/osu.Game.Modes.Osu/OsuRuleset.cs b/osu.Game.Modes.Osu/OsuRuleset.cs index 80b873db3a..d1bb5b02af 100644 --- a/osu.Game.Modes.Osu/OsuRuleset.cs +++ b/osu.Game.Modes.Osu/OsuRuleset.cs @@ -105,14 +105,10 @@ namespace osu.Game.Modes.Osu public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new OsuDifficultyCalculator(beatmap); - public override Score CreateAutoplayReplay(Beatmap beatmap) + public override Score CreateAutoplayScore(Beatmap beatmap) { - var processor = CreateScoreProcessor(); - - var score = processor.GetScore(); - + var score = CreateScoreProcessor().GetScore(); score.Replay = new OsuAutoReplay(beatmap); - return score; } diff --git a/osu.Game/Modes/Ruleset.cs b/osu.Game/Modes/Ruleset.cs index 2627f3faf0..7d5fc6ca77 100644 --- a/osu.Game/Modes/Ruleset.cs +++ b/osu.Game/Modes/Ruleset.cs @@ -43,7 +43,7 @@ namespace osu.Game.Modes public virtual FontAwesome Icon => FontAwesome.fa_question_circle; - public virtual Score CreateAutoplayReplay(Beatmap beatmap) => null; + public virtual Score CreateAutoplayScore(Beatmap beatmap) => null; public static Ruleset GetRuleset(PlayMode mode) { From 81cc27e1049ccf038dbee8790c5f3208ad58f0c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 10:06:25 +0900 Subject: [PATCH 16/34] Fix typo. --- osu.Game/Screens/Select/PlaySongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index f35f39a452..e51cdf136e 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Select (player = new PlayerLoader(new Player { - Beatmap = Beatmap, //eagerly set this so it's prsent before push. + Beatmap = Beatmap, //eagerly set this so it's present before push. })).LoadAsync(Game, l => Push(player)); } }, From cb002ce7af4966199dcc631a6fbacf2849b15343 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 10:10:29 +0900 Subject: [PATCH 17/34] General refactoring of OsuAutoReplay. --- osu.Game.Modes.Osu/OsuAutoReplay.cs | 87 +++++++++++++---------------- 1 file changed, 40 insertions(+), 47 deletions(-) diff --git a/osu.Game.Modes.Osu/OsuAutoReplay.cs b/osu.Game.Modes.Osu/OsuAutoReplay.cs index eb6e208059..8e1ea8dae2 100644 --- a/osu.Game.Modes.Osu/OsuAutoReplay.cs +++ b/osu.Game.Modes.Osu/OsuAutoReplay.cs @@ -15,6 +15,10 @@ namespace osu.Game.Modes.Osu { public class OsuAutoReplay : LegacyReplay { + static readonly Vector2 spinner_centre = new Vector2(256, 192); + + const float spin_radius = 50; + private Beatmap beatmap; public OsuAutoReplay(Beatmap beatmap) @@ -34,9 +38,9 @@ namespace osu.Game.Modes.Osu private static IComparer replayFrameComparer = new LegacyReplayFrameComparer(); - private static int FindInsertionIndex(List replay, LegacyReplayFrame frame) + private int findInsertionIndex(LegacyReplayFrame frame) { - int index = replay.BinarySearch(frame, replayFrameComparer); + int index = Frames.BinarySearch(frame, replayFrameComparer); if (index < 0) { @@ -45,7 +49,7 @@ namespace osu.Game.Modes.Osu else { // Go to the first index which is actually bigger - while (index < replay.Count && frame.Time == replay[index].Time) + while (index < Frames.Count && frame.Time == Frames[index].Time) { ++index; } @@ -54,15 +58,12 @@ namespace osu.Game.Modes.Osu return index; } - private static void AddFrameToReplay(List replay, LegacyReplayFrame frame) - { - replay.Insert(FindInsertionIndex(replay, frame), frame); - } + private void addFrameToReplay(LegacyReplayFrame frame) => Frames.Insert(findInsertionIndex(frame), frame); - private static Vector2 CirclePosition(double t, double radius) - { - return new Vector2((float)(Math.Cos(t) * radius), (float)(Math.Sin(t) * radius)); - } + private static Vector2 circlePosition(double t, double radius) => new Vector2((float)(Math.Cos(t) * radius), (float)(Math.Sin(t) * radius)); + + private double applyModsToTime(double v) => v; + private double applyModsToRate(double v) => v; private void createAutoReplay() { @@ -71,14 +72,12 @@ namespace osu.Game.Modes.Osu bool delayedMovements = false;// ModManager.CheckActive(Mods.Relax2); EasingTypes preferredEasing = delayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out; - AddFrameToReplay(Frames, new LegacyReplayFrame(-100000, 256, 500, LegacyButtonState.None)); - AddFrameToReplay(Frames, new LegacyReplayFrame(beatmap.HitObjects[0].StartTime - 1500, 256, 500, LegacyButtonState.None)); - AddFrameToReplay(Frames, new LegacyReplayFrame(beatmap.HitObjects[0].StartTime - 1000, 256, 192, LegacyButtonState.None)); + addFrameToReplay(new LegacyReplayFrame(-100000, 256, 500, LegacyButtonState.None)); + addFrameToReplay(new LegacyReplayFrame(beatmap.HitObjects[0].StartTime - 1500, 256, 500, LegacyButtonState.None)); + addFrameToReplay(new LegacyReplayFrame(beatmap.HitObjects[0].StartTime - 1000, 256, 192, LegacyButtonState.None)); // We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps. float frameDelay = (float)applyModsToRate(1000.0 / 60.0); - Vector2 spinnerCentre = new Vector2(256, 192); - const float spinnerRadius = 50; // Already superhuman, but still somewhat realistic int reactionTime = (int)applyModsToRate(100); @@ -103,18 +102,18 @@ namespace osu.Game.Modes.Osu //Make the cursor stay at a hitObject as long as possible (mainly for autopilot). if (h.StartTime - DrawableHitCircle.HITTABLE_RANGE > last.EndTime + DrawableHitCircle.HIT_WINDOW_50 + 50) { - if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) AddFrameToReplay(Frames, new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(Frames, new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HITTABLE_RANGE, h.Position.X, h.Position.Y, LegacyButtonState.None)); + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HITTABLE_RANGE, h.Position.X, h.Position.Y, LegacyButtonState.None)); } else if (h.StartTime - DrawableHitCircle.HIT_WINDOW_50 > last.EndTime + DrawableHitCircle.HIT_WINDOW_50 + 50) { - if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) AddFrameToReplay(Frames, new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(Frames, new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HIT_WINDOW_50, h.Position.X, h.Position.Y, LegacyButtonState.None)); + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HIT_WINDOW_50, h.Position.X, h.Position.Y, LegacyButtonState.None)); } else if (h.StartTime - DrawableHitCircle.HIT_WINDOW_100 > last.EndTime + DrawableHitCircle.HIT_WINDOW_100 + 50) { - if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) AddFrameToReplay(Frames, new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_100, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(Frames, new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HIT_WINDOW_100, h.Position.X, h.Position.Y, LegacyButtonState.None)); + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_100, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HIT_WINDOW_100, h.Position.X, h.Position.Y, LegacyButtonState.None)); } } @@ -128,14 +127,14 @@ namespace osu.Game.Modes.Osu targetPosition.X = Frames[Frames.Count - 1].MouseX; targetPosition.Y = Frames[Frames.Count - 1].MouseY; - Vector2 difference = spinnerCentre - targetPosition; + Vector2 difference = spinner_centre - targetPosition; float differenceLength = difference.Length; - float newLength = (float)Math.Sqrt(differenceLength * differenceLength - spinnerRadius * spinnerRadius); + float newLength = (float)Math.Sqrt(differenceLength * differenceLength - spin_radius * spin_radius); - if (differenceLength > spinnerRadius) + if (differenceLength > spin_radius) { - float angle = (float)Math.Asin(spinnerRadius / differenceLength); + float angle = (float)Math.Asin(spin_radius / differenceLength); if (angle > 0) { @@ -158,11 +157,11 @@ namespace osu.Game.Modes.Osu } else if (difference.Length > 0) { - targetPosition = spinnerCentre - difference * (spinnerRadius / difference.Length); + targetPosition = spinner_centre - difference * (spin_radius / difference.Length); } else { - targetPosition = spinnerCentre + new Vector2(0, -spinnerRadius); + targetPosition = spinner_centre + new Vector2(0, -spin_radius); } } @@ -177,7 +176,7 @@ namespace osu.Game.Modes.Osu if (waitTime > lastFrame.Time) { lastFrame = new LegacyReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState); - AddFrameToReplay(Frames, lastFrame); + addFrameToReplay(lastFrame); } Vector2 lastPosition = new Vector2(lastFrame.MouseX, lastFrame.MouseY); @@ -193,7 +192,7 @@ namespace osu.Game.Modes.Osu for (double time = lastFrame.Time + frameDelay; time < h.StartTime; time += frameDelay) { Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPosition, lastFrame.Time, h.StartTime, easing); - AddFrameToReplay(Frames, new LegacyReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState)); + addFrameToReplay(new LegacyReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState)); } buttonIndex = 0; @@ -205,19 +204,18 @@ namespace osu.Game.Modes.Osu } LegacyButtonState button = buttonIndex % 2 == 0 ? LegacyButtonState.Left1 : LegacyButtonState.Right1; - LegacyButtonState previousButton = LegacyButtonState.None; LegacyReplayFrame newFrame = new LegacyReplayFrame(h.StartTime, targetPosition.X, targetPosition.Y, button); LegacyReplayFrame endFrame = new LegacyReplayFrame(h.EndTime + endDelay, h.EndPosition.X, h.EndPosition.Y, LegacyButtonState.None); // Decrement because we want the previous frame, not the next one - int index = FindInsertionIndex(Frames, newFrame) - 1; + int index = findInsertionIndex(newFrame) - 1; // Do we have a previous frame? No need to check for < replay.Count since we decremented! if (index >= 0) { LegacyReplayFrame previousFrame = Frames[index]; - previousButton = previousFrame.ButtonState; + var previousButton = previousFrame.ButtonState; // If a button is already held, then we simply alternate if (previousButton != LegacyButtonState.None) @@ -232,7 +230,7 @@ namespace osu.Game.Modes.Osu } // We always follow the most recent slider / spinner, so remove any other frames that occur while it exists. - int endIndex = FindInsertionIndex(Frames, endFrame); + int endIndex = findInsertionIndex(endFrame); if (index < Frames.Count - 1) Frames.RemoveRange(index + 1, Math.Max(0, endIndex - (index + 1))); @@ -247,12 +245,12 @@ namespace osu.Game.Modes.Osu } } - AddFrameToReplay(Frames, newFrame); + addFrameToReplay(newFrame); // We add intermediate frames for spinning / following a slider here. if (h is Spinner) { - Vector2 difference = targetPosition - spinnerCentre; + Vector2 difference = targetPosition - spinner_centre; float radius = difference.Length; float angle = radius == 0 ? 0 : (float)Math.Atan2(difference.Y, difference.X); @@ -263,14 +261,14 @@ namespace osu.Game.Modes.Osu { t = applyModsToTime(j - h.StartTime) * spinnerDirection; - Vector2 pos = spinnerCentre + CirclePosition(t / 20 + angle, spinnerRadius); - AddFrameToReplay(Frames, new LegacyReplayFrame((int)j, pos.X, pos.Y, button)); + Vector2 pos = spinner_centre + circlePosition(t / 20 + angle, spin_radius); + addFrameToReplay(new LegacyReplayFrame((int)j, pos.X, pos.Y, button)); } t = applyModsToTime(h.EndTime - h.StartTime) * spinnerDirection; - Vector2 endPosition = spinnerCentre + CirclePosition(t / 20 + angle, spinnerRadius); + Vector2 endPosition = spinner_centre + circlePosition(t / 20 + angle, spin_radius); - AddFrameToReplay(Frames, new LegacyReplayFrame(h.EndTime, endPosition.X, endPosition.Y, button)); + addFrameToReplay(new LegacyReplayFrame(h.EndTime, endPosition.X, endPosition.Y, button)); endFrame.MouseX = endPosition.X; endFrame.MouseY = endPosition.Y; @@ -292,21 +290,16 @@ namespace osu.Game.Modes.Osu // lastTime = t.Time1; //} - AddFrameToReplay(Frames, new LegacyReplayFrame(h.EndTime, s.EndPosition.X, s.EndPosition.Y, button)); + addFrameToReplay(new LegacyReplayFrame(h.EndTime, s.EndPosition.X, s.EndPosition.Y, button)); } // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! if (Frames[Frames.Count - 1].Time <= endFrame.Time) - { - AddFrameToReplay(Frames, endFrame); - } + addFrameToReplay(endFrame); } //Player.currentScore.Replay = InputManager.ReplayScore.Replay; //Player.currentScore.PlayerName = "osu!"; } - - private double applyModsToTime(double v) => v; - private double applyModsToRate(double v) => v; } } From 56922b66be39193d207e1bcf2871ed02ff0e1171 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 11:11:29 +0900 Subject: [PATCH 18/34] Refactor sliders to have more central position/progress calculations. --- .../Objects/Drawables/DrawableSlider.cs | 7 ++--- osu.Game.Modes.Osu/Objects/Slider.cs | 27 ++++++++++++++++++- osu.Game.Modes.Osu/Objects/SliderCurve.cs | 6 ++--- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs index 907ce8da63..a3e78ed422 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs @@ -102,8 +102,8 @@ namespace osu.Game.Modes.Osu.Objects.Drawables double progress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); - int repeat = (int)(progress * slider.RepeatCount); - progress = (progress * slider.RepeatCount) % 1; + int repeat = slider.RepeatAt(progress); + progress = slider.CurveProgressAt(progress); if (repeat > currentRepeat) { @@ -112,9 +112,6 @@ namespace osu.Game.Modes.Osu.Objects.Drawables currentRepeat = repeat; } - if (repeat % 2 == 1) - progress = 1 - progress; - bouncer2.Position = slider.Curve.PositionAt(body.SnakedEnd ?? 0); //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. diff --git a/osu.Game.Modes.Osu/Objects/Slider.cs b/osu.Game.Modes.Osu/Objects/Slider.cs index 5f6c50fd72..88abb68d0a 100644 --- a/osu.Game.Modes.Osu/Objects/Slider.cs +++ b/osu.Game.Modes.Osu/Objects/Slider.cs @@ -14,7 +14,32 @@ namespace osu.Game.Modes.Osu.Objects { public override double EndTime => StartTime + RepeatCount * Curve.Length / Velocity; - public override Vector2 EndPosition => RepeatCount % 2 == 0 ? Position : Curve.PositionAt(1); + public override Vector2 EndPosition => PositionAt(1); + + /// + /// Computes the position on the slider at a given progress that ranges from 0 (beginning of the slider) + /// to 1 (end of the slider). This includes repeat logic. + /// + /// Ranges from 0 (beginning of the slider) to 1 (end of the slider). + /// + public Vector2 PositionAt(double progress) => Curve.PositionAt(CurveProgressAt(progress)); + + /// + /// Find the current progress along the curve, accounting for repeat logic. + /// + public double CurveProgressAt(double progress) + { + var p = progress * RepeatCount % 1; + if (RepeatAt(progress) % 2 == 1) + p = 1 - p; + return p; + } + + /// + /// Determine which repeat of the slider we are on at a given progress. + /// Range is 0..RepeatCount where 0 is the first run. + /// + public int RepeatAt(double progress) => (int)(progress * RepeatCount); private int stackHeight; public override int StackHeight diff --git a/osu.Game.Modes.Osu/Objects/SliderCurve.cs b/osu.Game.Modes.Osu/Objects/SliderCurve.cs index 3b52a41b8c..e37da355c0 100644 --- a/osu.Game.Modes.Osu/Objects/SliderCurve.cs +++ b/osu.Game.Modes.Osu/Objects/SliderCurve.cs @@ -186,10 +186,10 @@ namespace osu.Game.Modes.Osu.Objects } /// - /// Computes the position on the slider at a given progress that ranges from 0 (beginning of the slider) - /// to 1 (end of the slider). + /// Computes the position on the slider at a given progress that ranges from 0 (beginning of the curve) + /// to 1 (end of the curve). /// - /// Ranges from 0 (beginning of the slider) to 1 (end of the slider). + /// Ranges from 0 (beginning of the curve) to 1 (end of the curve). /// public Vector2 PositionAt(double progress) { From 910d9ccc001e5b8b3a2703b3d94779af96604845 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 11:11:42 +0900 Subject: [PATCH 19/34] Add proper slider following support to OsuAutoReplay. --- osu.Game.Modes.Osu/OsuAutoReplay.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game.Modes.Osu/OsuAutoReplay.cs b/osu.Game.Modes.Osu/OsuAutoReplay.cs index 8e1ea8dae2..7d9f35abc8 100644 --- a/osu.Game.Modes.Osu/OsuAutoReplay.cs +++ b/osu.Game.Modes.Osu/OsuAutoReplay.cs @@ -276,19 +276,12 @@ namespace osu.Game.Modes.Osu else if (h is Slider) { Slider s = h as Slider; - int lastTime = 0; - //foreach ( - // Transformation t in - // s..Transformations.FindAll( - // tr => tr.Type == TransformationType.Movement)) - //{ - // if (lastTime != 0 && t.Time1 - lastTime < frameDelay) continue; - - // AddFrameToReplay(Frames, new LegacyReplayFrame(t.Time1, t.StartVector.X, t.StartVector.Y, - // button)); - // lastTime = t.Time1; - //} + for (double j = frameDelay; j < s.Duration; j += frameDelay) + { + Vector2 pos = s.PositionAt(j / s.Duration); + addFrameToReplay(new LegacyReplayFrame(h.StartTime + j, pos.X, pos.Y, button)); + } addFrameToReplay(new LegacyReplayFrame(h.EndTime, s.EndPosition.X, s.EndPosition.Y, button)); } From 20fcb8848baabe17b5a805cf0cb9f03d76410fd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 12:58:14 +0900 Subject: [PATCH 20/34] Move constants to base OsuHitObject representation. --- .../Objects/Drawables/DrawableHitCircle.cs | 17 +++++--------- .../Objects/Drawables/Pieces/CirclePiece.cs | 2 +- osu.Game.Modes.Osu/Objects/OsuHitObject.cs | 22 +++++++++++++++++++ osu.Game.Modes.Osu/OsuAutoReplay.cs | 20 ++++++++--------- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs index 6c05cb0b17..15614511f1 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -69,18 +69,11 @@ namespace osu.Game.Modes.Osu.Objects.Drawables Size = circle.DrawSize; } - //todo: these aren't constants. - public const double HITTABLE_RANGE = 300; - public const double HIT_WINDOW_50 = 150; - public const double HIT_WINDOW_100 = 80; - public const double HIT_WINDOW_300 = 30; - public const double CIRCLE_RADIUS = 64; - protected override void CheckJudgement(bool userTriggered) { if (!userTriggered) { - if (Judgement.TimeOffset > HIT_WINDOW_50) + if (Judgement.TimeOffset > OsuHitObject.HIT_WINDOW_50) Judgement.Result = HitResult.Miss; return; } @@ -89,15 +82,15 @@ namespace osu.Game.Modes.Osu.Objects.Drawables OsuJudgementInfo osuJudgement = Judgement as OsuJudgementInfo; - if (hitOffset < HIT_WINDOW_50) + if (hitOffset < OsuHitObject.HIT_WINDOW_50) { Judgement.Result = HitResult.Hit; - if (hitOffset < HIT_WINDOW_300) + if (hitOffset < OsuHitObject.HIT_WINDOW_300) osuJudgement.Score = OsuScoreResult.Hit300; - else if (hitOffset < HIT_WINDOW_100) + else if (hitOffset < OsuHitObject.HIT_WINDOW_100) osuJudgement.Score = OsuScoreResult.Hit100; - else if (hitOffset < HIT_WINDOW_50) + else if (hitOffset < OsuHitObject.HIT_WINDOW_50) osuJudgement.Score = OsuScoreResult.Hit50; } else diff --git a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/CirclePiece.cs index 93974d1969..2a503e3dec 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -21,7 +21,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables.Pieces public CirclePiece() { - Size = new Vector2((float)DrawableHitCircle.CIRCLE_RADIUS * 2); + Size = new Vector2((float)OsuHitObject.OBJECT_RADIUS * 2); Masking = true; CornerRadius = Size.X / 2; diff --git a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs index cbf9a3de9c..aa12a83b4b 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs @@ -5,11 +5,18 @@ using System; using osu.Game.Modes.Objects; using OpenTK; using osu.Game.Beatmaps; +using osu.Game.Modes.Osu.Objects.Drawables; namespace osu.Game.Modes.Osu.Objects { public abstract class OsuHitObject : HitObject { + public const double HITTABLE_RANGE = 300; + public const double HIT_WINDOW_50 = 150; + public const double HIT_WINDOW_100 = 80; + public const double HIT_WINDOW_300 = 30; + public const double OBJECT_RADIUS = 64; + public Vector2 Position { get; set; } public Vector2 StackedPosition => Position + StackOffset; @@ -26,6 +33,21 @@ namespace osu.Game.Modes.Osu.Objects public abstract HitObjectType Type { get; } + public double HitWindowFor(OsuScoreResult result) + { + switch (result) + { + default: + return 300; + case OsuScoreResult.Hit50: + return 150; + case OsuScoreResult.Hit100: + return 80; + case OsuScoreResult.Hit300: + return 30; + } + } + public override void SetDefaultsFromBeatmap(Beatmap beatmap) { base.SetDefaultsFromBeatmap(beatmap); diff --git a/osu.Game.Modes.Osu/OsuAutoReplay.cs b/osu.Game.Modes.Osu/OsuAutoReplay.cs index 7d9f35abc8..b0df8b7dff 100644 --- a/osu.Game.Modes.Osu/OsuAutoReplay.cs +++ b/osu.Game.Modes.Osu/OsuAutoReplay.cs @@ -100,20 +100,20 @@ namespace osu.Game.Modes.Osu OsuHitObject last = beatmap.HitObjects[i - 1] as OsuHitObject; //Make the cursor stay at a hitObject as long as possible (mainly for autopilot). - if (h.StartTime - DrawableHitCircle.HITTABLE_RANGE > last.EndTime + DrawableHitCircle.HIT_WINDOW_50 + 50) + if (h.StartTime - OsuHitObject.HITTABLE_RANGE > last.EndTime + OsuHitObject.HIT_WINDOW_50 + 50) { - if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); - if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HITTABLE_RANGE, h.Position.X, h.Position.Y, LegacyButtonState.None)); + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + OsuHitObject.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - OsuHitObject.HITTABLE_RANGE, h.Position.X, h.Position.Y, LegacyButtonState.None)); } - else if (h.StartTime - DrawableHitCircle.HIT_WINDOW_50 > last.EndTime + DrawableHitCircle.HIT_WINDOW_50 + 50) + else if (h.StartTime - OsuHitObject.HIT_WINDOW_50 > last.EndTime + OsuHitObject.HIT_WINDOW_50 + 50) { - if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); - if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HIT_WINDOW_50, h.Position.X, h.Position.Y, LegacyButtonState.None)); + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + OsuHitObject.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - OsuHitObject.HIT_WINDOW_50, h.Position.X, h.Position.Y, LegacyButtonState.None)); } - else if (h.StartTime - DrawableHitCircle.HIT_WINDOW_100 > last.EndTime + DrawableHitCircle.HIT_WINDOW_100 + 50) + else if (h.StartTime - OsuHitObject.HIT_WINDOW_100 > last.EndTime + OsuHitObject.HIT_WINDOW_100 + 50) { - if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + DrawableHitCircle.HIT_WINDOW_100, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); - if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - DrawableHitCircle.HIT_WINDOW_100, h.Position.X, h.Position.Y, LegacyButtonState.None)); + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + OsuHitObject.HIT_WINDOW_100, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - OsuHitObject.HIT_WINDOW_100, h.Position.X, h.Position.Y, LegacyButtonState.None)); } } @@ -185,7 +185,7 @@ namespace osu.Game.Modes.Osu // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. if (timeDifference > 0 && // Sanity checks - ((lastPosition - targetPosition).Length > DrawableHitCircle.CIRCLE_RADIUS * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough + ((lastPosition - targetPosition).Length > OsuHitObject.OBJECT_RADIUS * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. { // Perform eased movement From faf07ab51a12069003b4235dc59269723dd41610 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 13:59:11 +0900 Subject: [PATCH 21/34] Use generics everywhere. --- .../Tests/TestCaseHitObjects.cs | 1 + osu.Game.Modes.Catch/UI/CatchHitRenderer.cs | 4 +- osu.Game.Modes.Catch/UI/CatchPlayfield.cs | 4 +- osu.Game.Modes.Mania/UI/ManiaHitRenderer.cs | 4 +- osu.Game.Modes.Mania/UI/ManiaPlayfield.cs | 4 +- .../Objects/Drawables/DrawableHitCircle.cs | 8 +-- .../Objects/Drawables/DrawableOsuHitObject.cs | 2 +- osu.Game.Modes.Osu/Objects/OsuHitObject.cs | 11 ++++ osu.Game.Modes.Osu/UI/OsuHitRenderer.cs | 4 +- osu.Game.Modes.Osu/UI/OsuPlayfield.cs | 9 +-- osu.Game.Modes.Taiko/UI/TaikoHitRenderer.cs | 4 +- osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs | 4 +- .../Objects/Drawables/DrawableHitObject.cs | 60 ++++++++++--------- osu.Game/Modes/UI/HitRenderer.cs | 36 +++++------ osu.Game/Modes/UI/Playfield.cs | 19 ++++-- osu.Game/Screens/Play/Player.cs | 11 ---- 16 files changed, 101 insertions(+), 84 deletions(-) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs index ba1b3b4f37..c1861b0874 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs @@ -13,6 +13,7 @@ using osu.Game.Modes.Osu.Objects.Drawables; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Game.Modes.Objects; using OpenTK.Graphics; namespace osu.Desktop.VisualTests.Tests diff --git a/osu.Game.Modes.Catch/UI/CatchHitRenderer.cs b/osu.Game.Modes.Catch/UI/CatchHitRenderer.cs index 0d06ce29a7..dd61fdd453 100644 --- a/osu.Game.Modes.Catch/UI/CatchHitRenderer.cs +++ b/osu.Game.Modes.Catch/UI/CatchHitRenderer.cs @@ -12,8 +12,8 @@ namespace osu.Game.Modes.Catch.UI { protected override HitObjectConverter Converter => new CatchConverter(); - protected override Playfield CreatePlayfield() => new CatchPlayfield(); + protected override Playfield CreatePlayfield() => new CatchPlayfield(); - protected override DrawableHitObject GetVisualRepresentation(CatchBaseHit h) => null;// new DrawableFruit(h); + protected override DrawableHitObject GetVisualRepresentation(CatchBaseHit h) => null;// new DrawableFruit(h); } } diff --git a/osu.Game.Modes.Catch/UI/CatchPlayfield.cs b/osu.Game.Modes.Catch/UI/CatchPlayfield.cs index cf69ab4fe2..ff57d72cb2 100644 --- a/osu.Game.Modes.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Modes.Catch/UI/CatchPlayfield.cs @@ -3,12 +3,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Modes.Catch.Objects; +using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.UI; using OpenTK; namespace osu.Game.Modes.Catch.UI { - public class CatchPlayfield : Playfield + public class CatchPlayfield : Playfield { public CatchPlayfield() { diff --git a/osu.Game.Modes.Mania/UI/ManiaHitRenderer.cs b/osu.Game.Modes.Mania/UI/ManiaHitRenderer.cs index 59c79f0d03..31bc4fffe4 100644 --- a/osu.Game.Modes.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Modes.Mania/UI/ManiaHitRenderer.cs @@ -19,9 +19,9 @@ namespace osu.Game.Modes.Mania.UI protected override HitObjectConverter Converter => new ManiaConverter(columns); - protected override Playfield CreatePlayfield() => new ManiaPlayfield(columns); + protected override Playfield CreatePlayfield() => new ManiaPlayfield(columns); - protected override DrawableHitObject GetVisualRepresentation(ManiaBaseHit h) + protected override DrawableHitObject GetVisualRepresentation(ManiaBaseHit h) { return null; //return new DrawableNote(h) diff --git a/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs b/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs index 2772b09e16..3a3abdaefd 100644 --- a/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs @@ -3,13 +3,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Modes.Mania.Objects; +using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.UI; using OpenTK; using OpenTK.Graphics; namespace osu.Game.Modes.Mania.UI { - public class ManiaPlayfield : Playfield + public class ManiaPlayfield : Playfield { private readonly int columns; diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs index 15614511f1..f88634f506 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -85,13 +85,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables if (hitOffset < OsuHitObject.HIT_WINDOW_50) { Judgement.Result = HitResult.Hit; - - if (hitOffset < OsuHitObject.HIT_WINDOW_300) - osuJudgement.Score = OsuScoreResult.Hit300; - else if (hitOffset < OsuHitObject.HIT_WINDOW_100) - osuJudgement.Score = OsuScoreResult.Hit100; - else if (hitOffset < OsuHitObject.HIT_WINDOW_50) - osuJudgement.Score = OsuScoreResult.Hit50; + osuJudgement.Score = HitObject.ScoreResultForOffset(hitOffset); } else Judgement.Result = HitResult.Miss; diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs index c7cc7cfdd7..4aa7b80278 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -7,7 +7,7 @@ using osu.Game.Modes.Objects.Drawables; namespace osu.Game.Modes.Osu.Objects.Drawables { - public class DrawableOsuHitObject : DrawableHitObject + public class DrawableOsuHitObject : DrawableHitObject { public const float TIME_PREEMPT = 600; public const float TIME_FADEIN = 400; diff --git a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs index aa12a83b4b..926182f0a4 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs @@ -48,6 +48,17 @@ namespace osu.Game.Modes.Osu.Objects } } + public OsuScoreResult ScoreResultForOffset(double offset) + { + if (offset < HitWindowFor(OsuScoreResult.Hit300)) + return OsuScoreResult.Hit300; + if (offset < HitWindowFor(OsuScoreResult.Hit100)) + return OsuScoreResult.Hit100; + if (offset < HitWindowFor(OsuScoreResult.Hit50)) + return OsuScoreResult.Hit50; + return OsuScoreResult.Miss; + } + public override void SetDefaultsFromBeatmap(Beatmap beatmap) { base.SetDefaultsFromBeatmap(beatmap); diff --git a/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs b/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs index fa222aafd7..0f232cee9b 100644 --- a/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs +++ b/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs @@ -13,9 +13,9 @@ namespace osu.Game.Modes.Osu.UI { protected override HitObjectConverter Converter => new OsuHitObjectConverter(); - protected override Playfield CreatePlayfield() => new OsuPlayfield(); + protected override Playfield CreatePlayfield() => new OsuPlayfield(); - protected override DrawableHitObject GetVisualRepresentation(OsuHitObject h) + protected override DrawableHitObject GetVisualRepresentation(OsuHitObject h) { if (h is HitCircle) return new DrawableHitCircle(h as HitCircle); diff --git a/osu.Game.Modes.Osu/UI/OsuPlayfield.cs b/osu.Game.Modes.Osu/UI/OsuPlayfield.cs index 7d439a96ef..2479b15dce 100644 --- a/osu.Game.Modes.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Modes.Osu/UI/OsuPlayfield.cs @@ -11,10 +11,11 @@ using osu.Game.Modes.Osu.Objects.Drawables.Connections; using osu.Game.Modes.UI; using System.Linq; using osu.Game.Graphics.Cursor; +using osu.Game.Modes.Objects; namespace osu.Game.Modes.Osu.UI { - public class OsuPlayfield : Playfield + public class OsuPlayfield : Playfield { private Container approachCircles; private Container judgementLayer; @@ -59,7 +60,7 @@ namespace osu.Game.Modes.Osu.UI }); } - public override void Add(DrawableHitObject h) + public override void Add(DrawableHitObject h) { h.Depth = (float)h.HitObject.StartTime; IDrawableHitObjectWithProxiedApproach c = h as IDrawableHitObjectWithProxiedApproach; @@ -80,9 +81,9 @@ namespace osu.Game.Modes.Osu.UI .OrderBy(h => h.StartTime); } - private void judgement(DrawableHitObject h, JudgementInfo j) + private void judgement(DrawableHitObject h, JudgementInfo j) { - HitExplosion explosion = new HitExplosion((OsuJudgementInfo)j, (OsuHitObject)h.HitObject); + HitExplosion explosion = new HitExplosion((OsuJudgementInfo)j, h.HitObject); judgementLayer.Add(explosion); } diff --git a/osu.Game.Modes.Taiko/UI/TaikoHitRenderer.cs b/osu.Game.Modes.Taiko/UI/TaikoHitRenderer.cs index 0e2b6cdc83..1b9bb682f1 100644 --- a/osu.Game.Modes.Taiko/UI/TaikoHitRenderer.cs +++ b/osu.Game.Modes.Taiko/UI/TaikoHitRenderer.cs @@ -12,8 +12,8 @@ namespace osu.Game.Modes.Taiko.UI { protected override HitObjectConverter Converter => new TaikoConverter(); - protected override Playfield CreatePlayfield() => new TaikoPlayfield(); + protected override Playfield CreatePlayfield() => new TaikoPlayfield(); - protected override DrawableHitObject GetVisualRepresentation(TaikoBaseHit h) => null;// new DrawableTaikoHit(h); + protected override DrawableHitObject GetVisualRepresentation(TaikoBaseHit h) => null;// new DrawableTaikoHit(h); } } diff --git a/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs index e8aa28e5b3..9c57ae8ad5 100644 --- a/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs @@ -5,13 +5,15 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Modes.Objects.Drawables; +using osu.Game.Modes.Taiko.Objects; using osu.Game.Modes.UI; using OpenTK; using OpenTK.Graphics; namespace osu.Game.Modes.Taiko.UI { - public class TaikoPlayfield : Playfield + public class TaikoPlayfield : Playfield { public TaikoPlayfield() { diff --git a/osu.Game/Modes/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Modes/Objects/Drawables/DrawableHitObject.cs index ca1967904a..2d78f8a04a 100644 --- a/osu.Game/Modes/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Modes/Objects/Drawables/DrawableHitObject.cs @@ -16,8 +16,6 @@ namespace osu.Game.Modes.Objects.Drawables { public abstract class DrawableHitObject : Container, IStateful { - public event Action OnJudgement; - public override bool HandleInput => Interactive; public bool Interactive = true; @@ -26,12 +24,7 @@ namespace osu.Game.Modes.Objects.Drawables public abstract JudgementInfo CreateJudgementInfo(); - public HitObject HitObject; - - public DrawableHitObject(HitObject hitObject) - { - HitObject = hitObject; - } + protected abstract void UpdateState(ArmedState state); private ArmedState state; public ArmedState State @@ -52,20 +45,11 @@ namespace osu.Game.Modes.Objects.Drawables } } - SampleChannel sample; - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - string hitType = ((HitObject.Sample?.Type ?? SampleType.None) == SampleType.None ? SampleType.Normal : HitObject.Sample.Type).ToString().ToLower(); - string sampleSet = (HitObject.Sample?.Set ?? SampleSet.Normal).ToString().ToLower(); - - sample = audio.Sample.Get($@"Gameplay/{sampleSet}-hit{hitType}"); - } + protected SampleChannel Sample; protected virtual void PlaySample() { - sample?.Play(); + Sample?.Play(); } protected override void LoadComplete() @@ -81,18 +65,18 @@ namespace osu.Game.Modes.Objects.Drawables Expire(true); } + } - private List nestedHitObjects; + public abstract class DrawableHitObject : DrawableHitObject + where HitObjectType : HitObject + { + public event Action, JudgementInfo> OnJudgement; - protected IEnumerable NestedHitObjects => nestedHitObjects; + public HitObjectType HitObject; - protected void AddNested(DrawableHitObject h) + public DrawableHitObject(HitObjectType hitObject) { - if (nestedHitObjects == null) - nestedHitObjects = new List(); - - h.OnJudgement += (d, j) => { OnJudgement?.Invoke(d, j); } ; - nestedHitObjects.Add(h); + HitObject = hitObject; } /// @@ -143,7 +127,27 @@ namespace osu.Game.Modes.Objects.Drawables UpdateJudgement(false); } - protected abstract void UpdateState(ArmedState state); + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + string hitType = ((HitObject.Sample?.Type ?? SampleType.None) == SampleType.None ? SampleType.Normal : HitObject.Sample.Type).ToString().ToLower(); + string sampleSet = (HitObject.Sample?.Set ?? SampleSet.Normal).ToString().ToLower(); + + Sample = audio.Sample.Get($@"Gameplay/{sampleSet}-hit{hitType}"); + } + + private List> nestedHitObjects; + + protected IEnumerable> NestedHitObjects => nestedHitObjects; + + protected void AddNested(DrawableHitObject h) + { + if (nestedHitObjects == null) + nestedHitObjects = new List>(); + + h.OnJudgement += (d, j) => { OnJudgement?.Invoke(d, j); } ; + nestedHitObjects.Add(h); + } } public enum ArmedState diff --git a/osu.Game/Modes/UI/HitRenderer.cs b/osu.Game/Modes/UI/HitRenderer.cs index 356c9b9276..eac53b811b 100644 --- a/osu.Game/Modes/UI/HitRenderer.cs +++ b/osu.Game/Modes/UI/HitRenderer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Modes.UI public event Action OnAllJudged; - public InputManager InputManager; + public abstract bool AllObjectsJudged { get; } protected void TriggerOnJudgement(JudgementInfo j) { @@ -28,18 +28,20 @@ namespace osu.Game.Modes.UI if (AllObjectsJudged) OnAllJudged?.Invoke(); } - - protected Playfield Playfield; - - public bool AllObjectsJudged => Playfield.HitObjects.Children.First()?.Judgement.Result != null; //reverse depth sort means First() instead of Last(). - - public IEnumerable DrawableObjects => Playfield.HitObjects.Children; } - public abstract class HitRenderer : HitRenderer - where T : HitObject + public abstract class HitRenderer : HitRenderer + where TObject : HitObject { - private List objects; + private List objects; + + public InputManager InputManager; + + protected Playfield Playfield; + + public override bool AllObjectsJudged => Playfield.HitObjects.Children.First()?.Judgement.Result != null; //reverse depth sort means First() instead of Last(). + + public IEnumerable DrawableObjects => Playfield.HitObjects.Children; public Beatmap Beatmap { @@ -51,11 +53,11 @@ namespace osu.Game.Modes.UI } } - protected abstract Playfield CreatePlayfield(); + protected abstract Playfield CreatePlayfield(); - protected abstract HitObjectConverter Converter { get; } + protected abstract HitObjectConverter Converter { get; } - protected virtual List Convert(Beatmap beatmap) => Converter.Convert(beatmap); + protected virtual List Convert(Beatmap beatmap) => Converter.Convert(beatmap); public HitRenderer() { @@ -76,9 +78,9 @@ namespace osu.Game.Modes.UI private void loadObjects() { if (objects == null) return; - foreach (T h in objects) + foreach (TObject h in objects) { - var drawableObject = GetVisualRepresentation(h); + DrawableHitObject drawableObject = GetVisualRepresentation(h); if (drawableObject == null) continue; @@ -89,8 +91,8 @@ namespace osu.Game.Modes.UI Playfield.PostProcess(); } - private void onJudgement(DrawableHitObject o, JudgementInfo j) => TriggerOnJudgement(j); + private void onJudgement(DrawableHitObject o, JudgementInfo j) => TriggerOnJudgement(j); - protected abstract DrawableHitObject GetVisualRepresentation(T h); + protected abstract DrawableHitObject GetVisualRepresentation(TObject h); } } diff --git a/osu.Game/Modes/UI/Playfield.cs b/osu.Game/Modes/UI/Playfield.cs index 6a8c311261..424b4c3c68 100644 --- a/osu.Game/Modes/UI/Playfield.cs +++ b/osu.Game/Modes/UI/Playfield.cs @@ -6,16 +6,25 @@ using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; +using osu.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; namespace osu.Game.Modes.UI { - public abstract class Playfield : Container + public abstract class Playfield : Container + where T : HitObject { - public HitObjectContainer HitObjects; - private Container scaledContent; + public HitObjectContainer> HitObjects; - public virtual void Add(DrawableHitObject h) => HitObjects.Add(h); + public virtual void Add(DrawableHitObject h) => HitObjects.Add(h); + + public class HitObjectContainer : Container + where U : Drawable + { + public override bool Contains(Vector2 screenSpacePos) => true; + } + + private Container scaledContent; public override bool Contains(Vector2 screenSpacePos) => true; @@ -37,7 +46,7 @@ namespace osu.Game.Modes.UI } }); - Add(HitObjects = new HitObjectContainer + Add(HitObjects = new HitObjectContainer> { RelativeSizeAxes = Axes.Both, }); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4cd919874e..644ed531a9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -4,12 +4,10 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Timing; using osu.Game.Database; using osu.Game.Modes; -using osu.Game.Modes.Objects.Drawables; using osu.Game.Screens.Backgrounds; using OpenTK; using osu.Framework.Screens; @@ -18,24 +16,18 @@ using osu.Game.Screens.Ranking; using osu.Game.Configuration; 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.Framework.Input.Handlers; -using osu.Game.Graphics.Cursor; using osu.Game.Input.Handlers; namespace osu.Game.Screens.Play { public class Player : OsuScreen { - public bool Autoplay; - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); internal override bool ShowOverlays => false; @@ -148,9 +140,6 @@ namespace osu.Game.Screens.Play //bind ScoreProcessor to ourselves (for a fail situation) scoreProcessor.Failed += onFail; - if (Autoplay) - hitRenderer.Schedule(() => hitRenderer.DrawableObjects.ForEach(h => h.State = ArmedState.Hit)); - Children = new Drawable[] { new Container From 3b0445a244b906336545248cadc0b098e5e7e2d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 14:08:58 +0900 Subject: [PATCH 22/34] Improve comment for PreferredPlayMode and allow null. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 0ee45d871b..50f8264ebe 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -19,12 +19,12 @@ namespace osu.Game.Beatmaps public readonly BeatmapSetInfo BeatmapSetInfo; /// - /// A play mode that is preferred for this beatmap. This allows for conversion between game modes where feasible, - /// but does not gurantee an outcome. + /// A play mode that is preferred for this beatmap. PlayMode will become this mode where conversion is feasible, + /// or otherwise to the beatmap's default. /// - public PlayMode PreferredPlayMode; + public PlayMode? PreferredPlayMode; - public PlayMode PlayMode => beatmap?.BeatmapInfo?.Mode > PlayMode.Osu ? beatmap.BeatmapInfo.Mode : PreferredPlayMode; + public PlayMode PlayMode => beatmap?.BeatmapInfo?.Mode > PlayMode.Osu || !PreferredPlayMode.HasValue ? beatmap.BeatmapInfo.Mode : PreferredPlayMode.Value; public readonly bool WithStoryboard; From 1b03998b8675bf31ded030c635fd518afa010832 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 14:15:43 +0900 Subject: [PATCH 23/34] Improve comment of SetFrameFromTime. --- osu.Game/Input/Handlers/ReplayInputHandler.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs index d933c68099..76e038048c 100644 --- a/osu.Game/Input/Handlers/ReplayInputHandler.cs +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -2,17 +2,9 @@ // 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 { @@ -29,7 +21,7 @@ namespace osu.Game.Input.Handlers /// 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. + /// The usable time value. If null, we should not advance time as we do not have enough data. public abstract double? SetFrameFromTime(double time); public override bool Initialize(GameHost host) => true; From 809828f0ba68244bbff5f2afe3e5a652c2f32100 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 14:18:44 +0900 Subject: [PATCH 24/34] Improve NextFrame. --- osu.Game/Modes/LegacyReplay.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Modes/LegacyReplay.cs b/osu.Game/Modes/LegacyReplay.cs index 0ed63b309e..c558280cb0 100644 --- a/osu.Game/Modes/LegacyReplay.cs +++ b/osu.Game/Modes/LegacyReplay.cs @@ -54,19 +54,22 @@ namespace osu.Game.Modes public class LegacyReplayInputHandler : ReplayInputHandler { 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 LegacyReplayFrame NextFrame => !hasFrames ? null : replayContent[nextFrameIndex]; + + int currentFrameIndex; + + private int nextFrameIndex => MathHelper.Clamp(currentFrameIndex + (currentDirection > 0 ? 1 : -1), 0, replayContent.Count - 1); public LegacyReplayInputHandler(List replayContent) { this.replayContent = replayContent; } - private bool nextFrame() + private bool advanceFrame() { - int newFrame = MathHelper.Clamp(currentFrameIndex + (currentDirection > 0 ? 1 : -1), 0, replayContent.Count - 1); + int newFrame = nextFrameIndex; //ensure we aren't at an extent. if (newFrame == currentFrameIndex) return false; @@ -158,7 +161,7 @@ namespace osu.Game.Modes if (hasFrames) { //if we changed frames, we want to execute once *exactly* on the frame's time. - if (currentDirection == time.CompareTo(NextFrame.Time) && nextFrame()) + if (currentDirection == time.CompareTo(NextFrame.Time) && advanceFrame()) 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. From 4118be6388f6d040e3edb3071c5af42ff0fcf192 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 14:19:22 +0900 Subject: [PATCH 25/34] Remove unnecessary bounds check. --- osu.Game/Modes/LegacyReplay.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Modes/LegacyReplay.cs b/osu.Game/Modes/LegacyReplay.cs index c558280cb0..cd271f181c 100644 --- a/osu.Game/Modes/LegacyReplay.cs +++ b/osu.Game/Modes/LegacyReplay.cs @@ -89,9 +89,6 @@ namespace osu.Game.Modes if (!hasFrames) return null; - if (AtLastFrame) - return CurrentFrame.Position; - return Interpolation.ValueAt(currentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time); } } From 652d18aadab260399bf07ac382e0560d04a84160 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 14:20:44 +0900 Subject: [PATCH 26/34] Update second usage of comment. --- osu.Game/Modes/LegacyReplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Modes/LegacyReplay.cs b/osu.Game/Modes/LegacyReplay.cs index cd271f181c..c081329906 100644 --- a/osu.Game/Modes/LegacyReplay.cs +++ b/osu.Game/Modes/LegacyReplay.cs @@ -149,7 +149,7 @@ namespace osu.Game.Modes /// 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. + /// The usable time value. If null, we should not advance time as we do not have enough data. public override double? SetFrameFromTime(double time) { currentDirection = time.CompareTo(currentTime); From 1ea21daa9105b5b3cd722da9a6fcf0f0de89dcff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 14:25:38 +0900 Subject: [PATCH 27/34] Fix PlayMode regression. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 50f8264ebe..00986bcff3 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps /// public PlayMode? PreferredPlayMode; - public PlayMode PlayMode => beatmap?.BeatmapInfo?.Mode > PlayMode.Osu || !PreferredPlayMode.HasValue ? beatmap.BeatmapInfo.Mode : PreferredPlayMode.Value; + public PlayMode PlayMode => beatmap?.BeatmapInfo?.Mode > PlayMode.Osu ? beatmap.BeatmapInfo.Mode : PreferredPlayMode ?? PlayMode.Osu; public readonly bool WithStoryboard; From 76ef8c1a6c9fab154b2e2415b43b1942cc40d370 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 14:52:37 +0900 Subject: [PATCH 28/34] Add bindable mods and autoplay support. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 3 +++ osu.Game/Modes/Mod.cs | 14 ++++++++++++++ osu.Game/Screens/Play/Player.cs | 21 ++++++++++++--------- osu.Game/Screens/Play/PlayerInputManager.cs | 1 + osu.Game/Screens/Select/PlaySongSelect.cs | 2 ++ 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 00986bcff3..3569eebb75 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -4,6 +4,7 @@ using System; using System.IO; using osu.Framework.Audio.Track; +using osu.Framework.Configuration; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.IO; @@ -26,6 +27,8 @@ namespace osu.Game.Beatmaps public PlayMode PlayMode => beatmap?.BeatmapInfo?.Mode > PlayMode.Osu ? beatmap.BeatmapInfo.Mode : PreferredPlayMode ?? PlayMode.Osu; + public readonly Bindable Mods = new Bindable(); + public readonly bool WithStoryboard; protected abstract ArchiveReader GetReader(); diff --git a/osu.Game/Modes/Mod.cs b/osu.Game/Modes/Mod.cs index e8d36b0aef..98504f1bac 100644 --- a/osu.Game/Modes/Mod.cs +++ b/osu.Game/Modes/Mod.cs @@ -4,6 +4,8 @@ using System; using System.ComponentModel; using osu.Game.Graphics; +using osu.Game.Modes.UI; +using osu.Game.Screens.Play; namespace osu.Game.Modes { @@ -41,6 +43,12 @@ namespace osu.Game.Modes /// The mods this mod cannot be enabled with. /// public abstract Mods[] DisablesMods { get; } + + /// + /// Direct access to the Player before load has run. + /// + /// + public virtual void PlayerLoading(Player player) { } } public class MultiMod : Mod @@ -150,6 +158,12 @@ namespace osu.Game.Modes public override double ScoreMultiplier => 0; public override bool Ranked => false; public override Mods[] DisablesMods => new Mods[] { Mods.Relax, Mods.Autopilot, Mods.SpunOut, Mods.SuddenDeath, Mods.Perfect }; + + public override void PlayerLoading(Player player) + { + base.PlayerLoading(player); + player.ReplayInputHandler = Ruleset.GetRuleset(player.Beatmap.PlayMode).CreateAutoplayScore(player.Beatmap.Beatmap)?.Replay?.GetInputHandler(); + } } public abstract class ModPerfect : ModSuddenDeath diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 644ed531a9..64fe737535 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -17,6 +17,7 @@ using osu.Game.Configuration; using osu.Framework.Configuration; using System; using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; using OpenTK.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; @@ -66,6 +67,17 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(AudioManager audio, BeatmapDatabase beatmaps, OsuGameBase game, OsuConfigManager config) { + var beatmap = Beatmap.Beatmap; + + if (beatmap.BeatmapInfo?.Mode > PlayMode.Osu) + { + //we only support osu! mode for now because the hitobject parsing is crappy and needs a refactor. + Exit(); + return; + } + + Beatmap.Mods.Value.ForEach(m => m.PlayerLoading(this)); + dimLevel = config.GetBindable(OsuConfig.DimLevel); mouseWheelDisabled = config.GetBindable(OsuConfig.MouseDisableWheel); @@ -102,15 +114,6 @@ namespace osu.Game.Screens.Play sourceClock.Reset(); }); - var beatmap = Beatmap.Beatmap; - - if (beatmap.BeatmapInfo?.Mode > PlayMode.Osu) - { - //we only support osu! mode for now because the hitobject parsing is crappy and needs a refactor. - Exit(); - return; - } - ruleset = Ruleset.GetRuleset(Beatmap.PlayMode); scoreOverlay = ruleset.CreateScoreOverlay(); diff --git a/osu.Game/Screens/Play/PlayerInputManager.cs b/osu.Game/Screens/Play/PlayerInputManager.cs index ac98b6533b..8441c3d4fe 100644 --- a/osu.Game/Screens/Play/PlayerInputManager.cs +++ b/osu.Game/Screens/Play/PlayerInputManager.cs @@ -8,6 +8,7 @@ using osu.Game.Configuration; using System.Linq; using osu.Framework.Input.Handlers; using osu.Framework.Timing; +using osu.Game.Beatmaps; using osu.Game.Input.Handlers; using OpenTK.Input; using KeyboardState = osu.Framework.Input.KeyboardState; diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index e51cdf136e..14e84af03a 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -337,6 +337,8 @@ namespace osu.Game.Screens.Select { base.OnBeatmapChanged(beatmap); + beatmap.Mods.BindTo(modSelect.SelectedMods); + //todo: change background in selectionChanged instead; support per-difficulty backgrounds. changeBackground(beatmap); carousel.SelectBeatmap(beatmap?.BeatmapInfo); From ff51af94ecf7da73c216c295cc6f0253044f28ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 15:03:58 +0900 Subject: [PATCH 29/34] Fail on drag drop operations with mixed files. --- osu.Desktop/OsuGameDesktop.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 9f25ed4efd..dc9656dc5e 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -78,10 +78,10 @@ namespace osu.Desktop if (isFile) { var paths = ((object[])e.Data.GetData(DataFormats.FileDrop)).Select(f => f.ToString()).ToArray(); - if (paths.Any(p => !allowed_extensions.Any(ext => p.EndsWith(ext)))) - e.Effect = DragDropEffects.None; - else + if (allowed_extensions.Any(ext => paths.All(p => p.EndsWith(ext)))) e.Effect = DragDropEffects.Copy; + else + e.Effect = DragDropEffects.None; } } } From 2de25c23b44faf41ad6a37f6e68d57f9d735a856 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 15:20:55 +0900 Subject: [PATCH 30/34] Make Mods IEnumerable. --- osu.Game/Beatmaps/WorkingBeatmap.cs | 3 ++- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 3569eebb75..1d4047ea45 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -2,6 +2,7 @@ // 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.Audio.Track; using osu.Framework.Configuration; @@ -27,7 +28,7 @@ namespace osu.Game.Beatmaps public PlayMode PlayMode => beatmap?.BeatmapInfo?.Mode > PlayMode.Osu ? beatmap.BeatmapInfo.Mode : PreferredPlayMode ?? PlayMode.Osu; - public readonly Bindable Mods = new Bindable(); + public readonly Bindable> Mods = new Bindable>(); public readonly bool WithStoryboard; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 03b3091066..1fa44dfd01 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Mods private FillFlowContainer modSectionsContainer; - public readonly Bindable SelectedMods = new Bindable(); + public readonly Bindable> SelectedMods = new Bindable>(); public readonly Bindable PlayMode = new Bindable(); From 610de4a34ce9dedee0737227e8a784d7048de185 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 15:24:00 +0900 Subject: [PATCH 31/34] Only show replay cursor when replay input is present. --- osu.Game.Modes.Catch/CatchRuleset.cs | 3 ++- osu.Game.Modes.Mania/ManiaRuleset.cs | 3 ++- osu.Game.Modes.Osu/OsuRuleset.cs | 3 ++- osu.Game.Modes.Osu/UI/OsuPlayfield.cs | 10 ++++++++-- osu.Game.Modes.Taiko/TaikoRuleset.cs | 3 ++- osu.Game/Modes/Ruleset.cs | 3 ++- osu.Game/Modes/UI/HitRenderer.cs | 3 ++- osu.Game/Modes/UI/Playfield.cs | 3 ++- 8 files changed, 22 insertions(+), 9 deletions(-) diff --git a/osu.Game.Modes.Catch/CatchRuleset.cs b/osu.Game.Modes.Catch/CatchRuleset.cs index 70d99efeca..a09b65e4d4 100644 --- a/osu.Game.Modes.Catch/CatchRuleset.cs +++ b/osu.Game.Modes.Catch/CatchRuleset.cs @@ -9,6 +9,7 @@ using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.UI; using osu.Game.Modes.UI; using osu.Game.Beatmaps; +using osu.Game.Screens.Play; namespace osu.Game.Modes.Catch { @@ -16,7 +17,7 @@ namespace osu.Game.Modes.Catch { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null) => new CatchHitRenderer + public override HitRenderer CreateHitRendererWith(Beatmap beatmap, PlayerInputManager input = null) => new CatchHitRenderer { Beatmap = beatmap, InputManager = input, diff --git a/osu.Game.Modes.Mania/ManiaRuleset.cs b/osu.Game.Modes.Mania/ManiaRuleset.cs index 94c2c0c31f..d4d8f10258 100644 --- a/osu.Game.Modes.Mania/ManiaRuleset.cs +++ b/osu.Game.Modes.Mania/ManiaRuleset.cs @@ -9,6 +9,7 @@ using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.UI; using osu.Game.Modes.UI; using osu.Game.Beatmaps; +using osu.Game.Screens.Play; namespace osu.Game.Modes.Mania { @@ -16,7 +17,7 @@ namespace osu.Game.Modes.Mania { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null) => new ManiaHitRenderer + public override HitRenderer CreateHitRendererWith(Beatmap beatmap, PlayerInputManager input = null) => new ManiaHitRenderer { Beatmap = beatmap, InputManager = input, diff --git a/osu.Game.Modes.Osu/OsuRuleset.cs b/osu.Game.Modes.Osu/OsuRuleset.cs index d1bb5b02af..cfa650b3c4 100644 --- a/osu.Game.Modes.Osu/OsuRuleset.cs +++ b/osu.Game.Modes.Osu/OsuRuleset.cs @@ -10,6 +10,7 @@ using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.Objects; using osu.Game.Modes.Osu.UI; using osu.Game.Modes.UI; +using osu.Game.Screens.Play; namespace osu.Game.Modes.Osu { @@ -17,7 +18,7 @@ namespace osu.Game.Modes.Osu { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null) => new OsuHitRenderer + public override HitRenderer CreateHitRendererWith(Beatmap beatmap, PlayerInputManager input = null) => new OsuHitRenderer { Beatmap = beatmap, InputManager = input diff --git a/osu.Game.Modes.Osu/UI/OsuPlayfield.cs b/osu.Game.Modes.Osu/UI/OsuPlayfield.cs index 2479b15dce..26cda012e5 100644 --- a/osu.Game.Modes.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Modes.Osu/UI/OsuPlayfield.cs @@ -11,7 +11,7 @@ using osu.Game.Modes.Osu.Objects.Drawables.Connections; using osu.Game.Modes.UI; using System.Linq; using osu.Game.Graphics.Cursor; -using osu.Game.Modes.Objects; +using OpenTK.Graphics; namespace osu.Game.Modes.Osu.UI { @@ -56,10 +56,16 @@ namespace osu.Game.Modes.Osu.UI RelativeSizeAxes = Axes.Both, Depth = -1, }, - new OsuCursorContainer() }); } + protected override void LoadComplete() + { + base.LoadComplete(); + if (InputManager.ReplayInputHandler != null) + Add(new OsuCursorContainer { Colour = Color4.LightYellow }); + } + public override void Add(DrawableHitObject h) { h.Depth = (float)h.HitObject.StartTime; diff --git a/osu.Game.Modes.Taiko/TaikoRuleset.cs b/osu.Game.Modes.Taiko/TaikoRuleset.cs index f9e7aeba21..baf3f15e4b 100644 --- a/osu.Game.Modes.Taiko/TaikoRuleset.cs +++ b/osu.Game.Modes.Taiko/TaikoRuleset.cs @@ -9,6 +9,7 @@ using osu.Game.Modes.Osu.UI; using osu.Game.Modes.Taiko.UI; using osu.Game.Modes.UI; using osu.Game.Beatmaps; +using osu.Game.Screens.Play; namespace osu.Game.Modes.Taiko { @@ -16,7 +17,7 @@ namespace osu.Game.Modes.Taiko { public override ScoreOverlay CreateScoreOverlay() => new OsuScoreOverlay(); - public override HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null) => new TaikoHitRenderer + public override HitRenderer CreateHitRendererWith(Beatmap beatmap, PlayerInputManager input = null) => new TaikoHitRenderer { Beatmap = beatmap, InputManager = input, diff --git a/osu.Game/Modes/Ruleset.cs b/osu.Game/Modes/Ruleset.cs index 7d5fc6ca77..2b10f0ba4f 100644 --- a/osu.Game/Modes/Ruleset.cs +++ b/osu.Game/Modes/Ruleset.cs @@ -9,6 +9,7 @@ using System.Collections.Concurrent; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Screens.Play; namespace osu.Game.Modes { @@ -31,7 +32,7 @@ namespace osu.Game.Modes public abstract ScoreProcessor CreateScoreProcessor(int hitObjectCount = 0); - public abstract HitRenderer CreateHitRendererWith(Beatmap beatmap, InputManager input = null); + public abstract HitRenderer CreateHitRendererWith(Beatmap beatmap, PlayerInputManager input = null); public abstract HitObjectParser CreateHitObjectParser(); diff --git a/osu.Game/Modes/UI/HitRenderer.cs b/osu.Game/Modes/UI/HitRenderer.cs index eac53b811b..7596a24557 100644 --- a/osu.Game/Modes/UI/HitRenderer.cs +++ b/osu.Game/Modes/UI/HitRenderer.cs @@ -11,6 +11,7 @@ using osu.Framework.Input; using osu.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; using osu.Game.Beatmaps; +using osu.Game.Screens.Play; namespace osu.Game.Modes.UI { @@ -35,7 +36,7 @@ namespace osu.Game.Modes.UI { private List objects; - public InputManager InputManager; + public PlayerInputManager InputManager; protected Playfield Playfield; diff --git a/osu.Game/Modes/UI/Playfield.cs b/osu.Game/Modes/UI/Playfield.cs index 424b4c3c68..cbd49abc5a 100644 --- a/osu.Game/Modes/UI/Playfield.cs +++ b/osu.Game/Modes/UI/Playfield.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; +using osu.Game.Screens.Play; namespace osu.Game.Modes.UI { @@ -55,7 +56,7 @@ namespace osu.Game.Modes.UI /// /// An optional inputManager to provide interactivity etc. /// - public InputManager InputManager; + public PlayerInputManager InputManager; [BackgroundDependencyLoader] private void load() From fc6bd386eab5884fc8d789928638b090b4694ce0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 6 Mar 2017 23:26:57 +0900 Subject: [PATCH 32/34] Fix remaining usage of hit window constants. --- .../Objects/Drawables/DrawableHitCircle.cs | 4 ++-- osu.Game.Modes.Osu/Objects/OsuHitObject.cs | 11 ++++++---- osu.Game.Modes.Osu/OsuAutoReplay.cs | 20 +++++++++---------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs index f88634f506..fd888ecac1 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -73,7 +73,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables { if (!userTriggered) { - if (Judgement.TimeOffset > OsuHitObject.HIT_WINDOW_50) + if (Judgement.TimeOffset > HitObject.HitWindowFor(OsuScoreResult.Hit50)) Judgement.Result = HitResult.Miss; return; } @@ -82,7 +82,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables OsuJudgementInfo osuJudgement = Judgement as OsuJudgementInfo; - if (hitOffset < OsuHitObject.HIT_WINDOW_50) + if (hitOffset < HitObject.HitWindowFor(OsuScoreResult.Hit50)) { Judgement.Result = HitResult.Hit; osuJudgement.Score = HitObject.ScoreResultForOffset(hitOffset); diff --git a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs index 926182f0a4..ddcdae7be0 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs @@ -11,12 +11,13 @@ namespace osu.Game.Modes.Osu.Objects { public abstract class OsuHitObject : HitObject { - public const double HITTABLE_RANGE = 300; - public const double HIT_WINDOW_50 = 150; - public const double HIT_WINDOW_100 = 80; - public const double HIT_WINDOW_300 = 30; public const double OBJECT_RADIUS = 64; + private const double hittable_range = 300; + private const double hit_window_50 = 150; + private const double hit_window_100 = 80; + private const double hit_window_300 = 30; + public Vector2 Position { get; set; } public Vector2 StackedPosition => Position + StackOffset; @@ -29,6 +30,8 @@ namespace osu.Game.Modes.Osu.Objects public Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f); + public double Radius => OBJECT_RADIUS * Scale; + public float Scale { get; set; } = 1; public abstract HitObjectType Type { get; } diff --git a/osu.Game.Modes.Osu/OsuAutoReplay.cs b/osu.Game.Modes.Osu/OsuAutoReplay.cs index b0df8b7dff..a0fa0904af 100644 --- a/osu.Game.Modes.Osu/OsuAutoReplay.cs +++ b/osu.Game.Modes.Osu/OsuAutoReplay.cs @@ -100,20 +100,20 @@ namespace osu.Game.Modes.Osu OsuHitObject last = beatmap.HitObjects[i - 1] as OsuHitObject; //Make the cursor stay at a hitObject as long as possible (mainly for autopilot). - if (h.StartTime - OsuHitObject.HITTABLE_RANGE > last.EndTime + OsuHitObject.HIT_WINDOW_50 + 50) + if (h.StartTime - h.HitWindowFor(OsuScoreResult.Miss) > last.EndTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50) { - if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + OsuHitObject.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); - if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - OsuHitObject.HITTABLE_RANGE, h.Position.X, h.Position.Y, LegacyButtonState.None)); + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Miss), h.Position.X, h.Position.Y, LegacyButtonState.None)); } - else if (h.StartTime - OsuHitObject.HIT_WINDOW_50 > last.EndTime + OsuHitObject.HIT_WINDOW_50 + 50) + else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50) > last.EndTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50) { - if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + OsuHitObject.HIT_WINDOW_50, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); - if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - OsuHitObject.HIT_WINDOW_50, h.Position.X, h.Position.Y, LegacyButtonState.None)); + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50), h.Position.X, h.Position.Y, LegacyButtonState.None)); } - else if (h.StartTime - OsuHitObject.HIT_WINDOW_100 > last.EndTime + OsuHitObject.HIT_WINDOW_100 + 50) + else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100) > last.EndTime + h.HitWindowFor(OsuScoreResult.Hit100) + 50) { - if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + OsuHitObject.HIT_WINDOW_100, last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); - if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - OsuHitObject.HIT_WINDOW_100, h.Position.X, h.Position.Y, LegacyButtonState.None)); + if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) addFrameToReplay(new LegacyReplayFrame(last.EndTime + h.HitWindowFor(OsuScoreResult.Hit100), last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None)); + if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100), h.Position.X, h.Position.Y, LegacyButtonState.None)); } } @@ -185,7 +185,7 @@ namespace osu.Game.Modes.Osu // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. if (timeDifference > 0 && // Sanity checks - ((lastPosition - targetPosition).Length > OsuHitObject.OBJECT_RADIUS * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough + ((lastPosition - targetPosition).Length > h.Radius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. { // Perform eased movement From 12c316aba4dfe9e0e9f82afb4ab79d769c0795e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Mar 2017 11:29:55 +0900 Subject: [PATCH 33/34] Fix int truncation. --- osu.Game/Modes/LegacyReplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Modes/LegacyReplay.cs b/osu.Game/Modes/LegacyReplay.cs index c081329906..0c9fb6ffca 100644 --- a/osu.Game/Modes/LegacyReplay.cs +++ b/osu.Game/Modes/LegacyReplay.cs @@ -123,7 +123,7 @@ namespace osu.Game.Modes public Vector2 Size => new Vector2(512, 384); - private const double sixty_frame_time = 1000 / 60; + private const double sixty_frame_time = 1000.0 / 60; double currentTime; int currentDirection; From 5cbcf7a20a36cc24344e35e165270d0d815dbf3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 7 Mar 2017 13:50:24 +0900 Subject: [PATCH 34/34] Fix error-level inspections. --- osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs | 1 - osu.Game.Modes.Catch/CatchRuleset.cs | 3 +-- osu.Game.Modes.Catch/UI/CatchPlayfield.cs | 1 - osu.Game.Modes.Mania/ManiaRuleset.cs | 3 +-- osu.Game.Modes.Mania/UI/ManiaPlayfield.cs | 1 - osu.Game.Modes.Osu/OsuRuleset.cs | 1 - osu.Game.Modes.Taiko/TaikoRuleset.cs | 3 +-- osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs | 1 - osu.Game/Database/BeatmapDatabase.cs | 1 - osu.Game/Modes/Mod.cs | 1 - osu.Game/Modes/Ruleset.cs | 1 - osu.Game/Modes/Score.cs | 2 -- osu.Game/Modes/UI/HitRenderer.cs | 1 - osu.Game/Modes/UI/Playfield.cs | 9 +++------ osu.Game/Screens/Play/PlayerInputManager.cs | 2 -- 15 files changed, 6 insertions(+), 25 deletions(-) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs index e98978bae2..aa76d8f98a 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs @@ -13,7 +13,6 @@ using osu.Game.Modes.Osu.Objects.Drawables; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; -using osu.Game.Modes.Objects; using OpenTK.Graphics; namespace osu.Desktop.VisualTests.Tests diff --git a/osu.Game.Modes.Catch/CatchRuleset.cs b/osu.Game.Modes.Catch/CatchRuleset.cs index a09b65e4d4..16c9114c66 100644 --- a/osu.Game.Modes.Catch/CatchRuleset.cs +++ b/osu.Game.Modes.Catch/CatchRuleset.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Modes.Catch.UI; using osu.Game.Modes.Objects; @@ -82,7 +81,7 @@ namespace osu.Game.Modes.Catch public override FontAwesome Icon => FontAwesome.fa_osu_fruits_o; - public override ScoreProcessor CreateScoreProcessor(int hitObjectCount) => null; + public override ScoreProcessor CreateScoreProcessor(int hitObjectCount = 0) => null; public override HitObjectParser CreateHitObjectParser() => new NullHitObjectParser(); diff --git a/osu.Game.Modes.Catch/UI/CatchPlayfield.cs b/osu.Game.Modes.Catch/UI/CatchPlayfield.cs index ff57d72cb2..c9c3df8197 100644 --- a/osu.Game.Modes.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Modes.Catch/UI/CatchPlayfield.cs @@ -4,7 +4,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Modes.Catch.Objects; -using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.UI; using OpenTK; diff --git a/osu.Game.Modes.Mania/ManiaRuleset.cs b/osu.Game.Modes.Mania/ManiaRuleset.cs index d4d8f10258..d4b5ecad22 100644 --- a/osu.Game.Modes.Mania/ManiaRuleset.cs +++ b/osu.Game.Modes.Mania/ManiaRuleset.cs @@ -1,7 +1,6 @@ // 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 System.Collections.Generic; using osu.Game.Graphics; using osu.Game.Modes.Mania.UI; @@ -98,7 +97,7 @@ namespace osu.Game.Modes.Mania public override FontAwesome Icon => FontAwesome.fa_osu_mania_o; - public override ScoreProcessor CreateScoreProcessor(int hitObjectCount) => null; + public override ScoreProcessor CreateScoreProcessor(int hitObjectCount = 0) => null; public override HitObjectParser CreateHitObjectParser() => new NullHitObjectParser(); diff --git a/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs b/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs index 75b1f09022..ab3c231917 100644 --- a/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs @@ -4,7 +4,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Modes.Mania.Objects; -using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.UI; using OpenTK; using OpenTK.Graphics; diff --git a/osu.Game.Modes.Osu/OsuRuleset.cs b/osu.Game.Modes.Osu/OsuRuleset.cs index cfa650b3c4..6f1b82ac5d 100644 --- a/osu.Game.Modes.Osu/OsuRuleset.cs +++ b/osu.Game.Modes.Osu/OsuRuleset.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Modes.Objects; diff --git a/osu.Game.Modes.Taiko/TaikoRuleset.cs b/osu.Game.Modes.Taiko/TaikoRuleset.cs index baf3f15e4b..73e65a2734 100644 --- a/osu.Game.Modes.Taiko/TaikoRuleset.cs +++ b/osu.Game.Modes.Taiko/TaikoRuleset.cs @@ -1,7 +1,6 @@ // 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 System.Collections.Generic; using osu.Game.Graphics; using osu.Game.Modes.Objects; @@ -82,7 +81,7 @@ namespace osu.Game.Modes.Taiko public override FontAwesome Icon => FontAwesome.fa_osu_taiko_o; - public override ScoreProcessor CreateScoreProcessor(int hitObjectCount) => null; + public override ScoreProcessor CreateScoreProcessor(int hitObjectCount = 0) => null; public override HitObjectParser CreateHitObjectParser() => new NullHitObjectParser(); diff --git a/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs index 9c57ae8ad5..2fcfce4fa3 100644 --- a/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.Taiko.Objects; using osu.Game.Modes.UI; using OpenTK; diff --git a/osu.Game/Database/BeatmapDatabase.cs b/osu.Game/Database/BeatmapDatabase.cs index e03625a2c7..871251b882 100644 --- a/osu.Game/Database/BeatmapDatabase.cs +++ b/osu.Game/Database/BeatmapDatabase.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; -using System.Security.Cryptography; using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Platform; diff --git a/osu.Game/Modes/Mod.cs b/osu.Game/Modes/Mod.cs index 217d1ad476..42bf3f6def 100644 --- a/osu.Game/Modes/Mod.cs +++ b/osu.Game/Modes/Mod.cs @@ -4,7 +4,6 @@ using System; using System.ComponentModel; using osu.Game.Graphics; -using osu.Game.Modes.UI; using osu.Game.Screens.Play; namespace osu.Game.Modes diff --git a/osu.Game/Modes/Ruleset.cs b/osu.Game/Modes/Ruleset.cs index 2b10f0ba4f..e145e60b97 100644 --- a/osu.Game/Modes/Ruleset.cs +++ b/osu.Game/Modes/Ruleset.cs @@ -6,7 +6,6 @@ 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; using osu.Game.Screens.Play; diff --git a/osu.Game/Modes/Score.cs b/osu.Game/Modes/Score.cs index 64dda4e181..1a761bea5d 100644 --- a/osu.Game/Modes/Score.cs +++ b/osu.Game/Modes/Score.cs @@ -1,9 +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.Handlers; using osu.Game.Database; -using SQLite.Net; namespace osu.Game.Modes { diff --git a/osu.Game/Modes/UI/HitRenderer.cs b/osu.Game/Modes/UI/HitRenderer.cs index e787993f26..cf5c2460d2 100644 --- a/osu.Game/Modes/UI/HitRenderer.cs +++ b/osu.Game/Modes/UI/HitRenderer.cs @@ -7,7 +7,6 @@ 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; diff --git a/osu.Game/Modes/UI/Playfield.cs b/osu.Game/Modes/UI/Playfield.cs index dfc0ba1bd0..502e072603 100644 --- a/osu.Game/Modes/UI/Playfield.cs +++ b/osu.Game/Modes/UI/Playfield.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using OpenTK; 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.Screens.Play; @@ -31,8 +30,6 @@ namespace osu.Game.Modes.UI protected override Container Content { get; } - private Container content; - public Playfield() { AddInternal(scaledContent = new ScaledContainer @@ -40,7 +37,7 @@ namespace osu.Game.Modes.UI RelativeSizeAxes = Axes.Both, Children = new[] { - content = new Container + Content = new Container { RelativeSizeAxes = Axes.Both, } @@ -64,9 +61,9 @@ namespace osu.Game.Modes.UI if (InputManager != null) { //if we've been provided an InputManager, we want it to sit inside the scaledcontainer - scaledContent.Remove(content); + scaledContent.Remove(Content); scaledContent.Add(InputManager); - InputManager.Add(content); + InputManager.Add(Content); } } diff --git a/osu.Game/Screens/Play/PlayerInputManager.cs b/osu.Game/Screens/Play/PlayerInputManager.cs index 8441c3d4fe..6d669e5169 100644 --- a/osu.Game/Screens/Play/PlayerInputManager.cs +++ b/osu.Game/Screens/Play/PlayerInputManager.cs @@ -6,9 +6,7 @@ using osu.Framework.Configuration; using osu.Framework.Input; using osu.Game.Configuration; using System.Linq; -using osu.Framework.Input.Handlers; using osu.Framework.Timing; -using osu.Game.Beatmaps; using osu.Game.Input.Handlers; using OpenTK.Input; using KeyboardState = osu.Framework.Input.KeyboardState;