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 +