1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 15:03:13 +08:00

Basic partial replay support.

This commit is contained in:
Dean Herbert 2017-02-28 20:14:48 +09:00
parent 327300e9a7
commit 58ae9e888d
No known key found for this signature in database
GPG Key ID: 46D71BF4958ABB49
19 changed files with 984 additions and 40 deletions

@ -1 +1 @@
Subproject commit 4c0762eec20d2a3063df908a49432326570bea9f
Subproject commit b32d1542d45c02c39e91bd0ebf8cc79aade9dd63

View File

@ -0,0 +1,15 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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
{
}
}

View File

@ -186,6 +186,7 @@
<Compile Include="Tests\TestCaseHitObjects.cs" />
<Compile Include="Tests\TestCaseKeyCounter.cs" />
<Compile Include="Tests\TestCaseMenuButtonSystem.cs" />
<Compile Include="Tests\TestCaseReplay.cs" />
<Compile Include="Tests\TestCaseScoreCounter.cs" />
<Compile Include="Tests\TestCaseTextAwesome.cs" />
<Compile Include="Tests\TestCasePlaySongSelect.cs" />

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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;

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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;

View File

@ -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<BeatmapStatistic> GetBeatmapStatistics(WorkingBeatmap beatmap) => new[]
{

View File

@ -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()
});
}

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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;

View File

@ -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<double> cursorScale;

View File

@ -0,0 +1,11 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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);
}
}

View File

@ -0,0 +1,279 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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
{
/// <summary> SerializationReader. Extends BinaryReader to add additional data types,
/// handle null strings and simplify use with ISerializable. </summary>
public class SerializationReader : BinaryReader
{
Stream stream;
public SerializationReader(Stream s)
: base(s, Encoding.UTF8)
{
stream = s;
}
public int RemainingBytes => (int)(stream.Length - stream.Position);
/// <summary> Static method to take a SerializationInfo object (an input to an ISerializable constructor)
/// and produce a SerializationReader from which serialized objects can be read </summary>.
public static SerializationReader GetReader(SerializationInfo info)
{
byte[] byteArray = (byte[])info.GetValue("X", typeof(byte[]));
MemoryStream ms = new MemoryStream(byteArray);
return new SerializationReader(ms);
}
/// <summary> Reads a string from the buffer. Overrides the base implementation so it can cope with nulls. </summary>
public override string ReadString()
{
if (0 == ReadByte()) return null;
return base.ReadString();
}
/// <summary> Reads a byte array from the buffer, handling nulls and the array length. </summary>
public byte[] ReadByteArray()
{
int len = ReadInt32();
if (len > 0) return ReadBytes(len);
if (len < 0) return null;
return new byte[0];
}
/// <summary> Reads a char array from the buffer, handling nulls and the array length. </summary>
public char[] ReadCharArray()
{
int len = ReadInt32();
if (len > 0) return ReadChars(len);
if (len < 0) return null;
return new char[0];
}
/// <summary> Reads a DateTime from the buffer. </summary>
public DateTime ReadDateTime()
{
long ticks = ReadInt64();
if (ticks < 0) throw new AbandonedMutexException("oops");
return new DateTime(ticks, DateTimeKind.Utc);
}
/// <summary> Reads a generic list from the buffer. </summary>
public IList<T> ReadBList<T>(bool skipErrors = false) where T : ILegacySerializable, new()
{
int count = ReadInt32();
if (count < 0) return null;
IList<T> d = new List<T>(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;
}
/// <summary> Reads a generic list from the buffer. </summary>
public IList<T> ReadList<T>()
{
int count = ReadInt32();
if (count < 0) return null;
IList<T> d = new List<T>(count);
for (int i = 0; i < count; i++) d.Add((T)ReadObject());
return d;
}
/// <summary> Reads a generic Dictionary from the buffer. </summary>
public IDictionary<T, U> ReadDictionary<T, U>()
{
int count = ReadInt32();
if (count < 0) return null;
IDictionary<T, U> d = new Dictionary<T, U>();
for (int i = 0; i < count; i++) d[(T)ReadObject()] = (U)ReadObject();
return d;
}
/// <summary> Reads an object which was added to the buffer by WriteObject. </summary>
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<string, Type> cache = new Dictionary<string, Type>();
public override Type BindToType(string assemblyName, string typeName)
{
Type typeToDeserialize;
if (cache.TryGetValue(assemblyName + typeName, out typeToDeserialize))
return typeToDeserialize;
List<Type> tmpTypes = new List<Type>();
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
}
}

View File

@ -0,0 +1,255 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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
{
/// <summary> SerializationWriter. Extends BinaryWriter to add additional data types,
/// handle null strings and simplify use with ISerializable. </summary>
public class SerializationWriter : BinaryWriter
{
public SerializationWriter(Stream s)
: base(s, Encoding.UTF8)
{
}
/// <summary> Static method to initialise the writer with a suitable MemoryStream. </summary>
public static SerializationWriter GetWriter()
{
MemoryStream ms = new MemoryStream(1024);
return new SerializationWriter(ms);
}
/// <summary> Writes a string to the buffer. Overrides the base implementation so it can cope with nulls </summary>
public override void Write(string str)
{
if (str == null)
{
Write((byte)ObjType.nullType);
}
else
{
Write((byte)ObjType.stringType);
base.Write(str);
}
}
/// <summary> 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 </summary>
public override void Write(byte[] b)
{
if (b == null)
{
Write(-1);
}
else
{
int len = b.Length;
Write(len);
if (len > 0) base.Write(b);
}
}
/// <summary> 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. </summary>
public override void Write(char[] c)
{
if (c == null)
{
Write(-1);
}
else
{
int len = c.Length;
Write(len);
if (len > 0) base.Write(c);
}
}
/// <summary> Writes a DateTime to the buffer. <summary>
public void Write(DateTime dt)
{
Write(dt.ToUniversalTime().Ticks);
}
/// <summary> Writes a generic ICollection (such as an IList<T>) to the buffer. </summary>
public void Write<T>(List<T> 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);
}
}
/// <summary> Writes a generic IDictionary to the buffer. </summary>
public void Write<T, U>(IDictionary<T, U> d)
{
if (d == null)
{
Write(-1);
}
else
{
Write(d.Count);
foreach (KeyValuePair<T, U> kvp in d)
{
WriteObject(kvp.Key);
WriteObject(kvp.Value);
}
}
}
/// <summary> 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. </summary>
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
/// <summary> Adds the SerializationWriter buffer to the SerializationInfo at the end of GetObjectData(). </summary>
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));
}
}
}

View File

@ -0,0 +1,244 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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
{
/// <summary>
/// The ReplayHandler will take a replay and handle the propagation of updates to the input stack.
/// It handles logic of any frames which *must* be executed.
/// </summary>
public class LegacyReplayInputHandler : InputHandler
{
public Func<Vector2, Vector2> ToScreenSpace { private get; set; }
private readonly List<LegacyReplayFrame> 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<LegacyReplayFrame> 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<InputState> GetPendingStates()
{
return new List<InputState>
{
new InputState
{
Mouse = new ReplayMouseState(
ToScreenSpace(position ?? Vector2.Zero),
new List<MouseState.ButtonState>
{
new MouseState.ButtonState(MouseButton.Left) { State = CurrentFrame?.MouseLeft ?? false },
new MouseState.ButtonState(MouseButton.Right) { State = CurrentFrame?.MouseRight ?? false },
}
),
Keyboard = new ReplayKeyboardState(new List<Key>())
}
};
}
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;
/// <summary>
/// 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).
/// </summary>
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;
/// <summary>
/// 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.
/// </summary>
/// <param name="time">The time which we should use for finding the current frame.</param>
/// <returns>The usable time value. If null, we shouldn't be running components reliant on this data.</returns>
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<ButtonState> list)
{
Position = position;
ButtonStates = list;
}
}
private class ReplayKeyboardState : KeyboardState
{
public ReplayKeyboardState(List<Key> 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}";
}
}
}
}

View File

@ -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();

View File

@ -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();
}

View File

@ -1,9 +1,11 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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<Drawable> content;
private Container<Drawable> scaledContent;
public virtual void Add(DrawableHitObject h) => HitObjects.Add(h);
@ -19,11 +21,20 @@ namespace osu.Game.Modes.UI
protected override Container<Drawable> 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
});
}
/// <summary>
/// An optional inputManager to provide interactivity etc.
/// </summary>
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()
{
}

View File

@ -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<LegacyReplayInputHandler.LegacyReplayFrame>
{
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(?)
}
});
}

View File

@ -1,46 +1,75 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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<bool> 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<bool> mouseDisabled;
protected override void LoadComplete()
{
base.LoadComplete();
parentClock = Clock;
Clock = new FramedClock(clock);
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
mouseDisabled = config.GetBindable<bool>(OsuConfig.MouseDisableButtons)
?? new Bindable<bool>(false);
mouseDisabled = config.GetBindable<bool>(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();
}
}
}
}

View File

@ -79,6 +79,10 @@
<Compile Include="Graphics\UserInterface\OsuSliderBar.cs" />
<Compile Include="Graphics\UserInterface\OsuTextBox.cs" />
<Compile Include="Graphics\UserInterface\TwoLayerButton.cs" />
<Compile Include="Input\Handlers\LegacyReplayInputHandler.cs" />
<Compile Include="IO\Legacy\ILegacySerializable.cs" />
<Compile Include="IO\Legacy\SerializationReader.cs" />
<Compile Include="IO\Legacy\SerializationWriter.cs" />
<Compile Include="Modes\Objects\Drawables\IDrawableHitObjectWithProxiedApproach.cs" />
<Compile Include="Modes\Objects\HitObjectParser.cs" />
<Compile Include="Modes\Objects\NullHitObjectParser.cs" />