diff --git a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs index 6a004f6614..54d715ae64 100644 --- a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs +++ b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs @@ -20,23 +20,28 @@ namespace osu.Desktop.VisualTests.Tests { internal class TestCasePlayer : TestCase { - private WorkingBeatmap beatmap; + protected Player Player; + private BeatmapDatabase db; + public override string Description => @"Showing everything to play the game."; [BackgroundDependencyLoader] private void load(BeatmapDatabase db) { - // ReSharper disable once ReplaceWithSingleCallToFirstOrDefault (TableQuery doesn't have correct LINQ implementation for First/FirstOrDefault). - 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 +87,20 @@ 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 + { + Beatmap = beatmap + }; + } + private class TestWorkingBeatmap : WorkingBeatmap { public TestWorkingBeatmap(Beatmap beatmap) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs new file mode 100644 index 0000000000..64b33a78c9 --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs @@ -0,0 +1,40 @@ +// 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.Allocation; +using osu.Framework.Input.Handlers; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Modes; +using osu.Game.Screens.Play; + +namespace osu.Desktop.VisualTests.Tests +{ + class TestCaseReplay : TestCasePlayer + { + private WorkingBeatmap beatmap; + + private InputHandler replay; + + private Func getReplayStream; + private ScoreDatabase scoreDatabase; + + public override string Description => @"Testing replay playback."; + + [BackgroundDependencyLoader] + private void load(Storage storage) + { + scoreDatabase = new ScoreDatabase(storage); + } + + protected override Player CreatePlayer(WorkingBeatmap beatmap) + { + var player = base.CreatePlayer(beatmap); + player.ReplayInputHandler = Ruleset.GetRuleset(beatmap.PlayMode).CreateAutoplayScore(beatmap.Beatmap)?.Replay?.GetInputHandler(); + return player; + } + } +} diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index 569a97e496..bea56b5cd0 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 @@ -187,6 +191,7 @@ + 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/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 1998dbb0a4..6d5cab14d6 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -9,6 +9,8 @@ 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; namespace osu.Desktop @@ -52,19 +54,32 @@ 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(); - ImportBeatmapsAsync(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(); - e.Effect = paths.Any(p => !p.EndsWith(".osz")) ? DragDropEffects.None : DragDropEffects.Copy; + var paths = ((object[])e.Data.GetData(DataFormats.FileDrop)).Select(f => f.ToString()).ToArray(); + if (allowed_extensions.Any(ext => paths.All(p => p.EndsWith(ext)))) + e.Effect = DragDropEffects.Copy; + else + e.Effect = DragDropEffects.None; } } } 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.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.Modes.Catch/CatchRuleset.cs b/osu.Game.Modes.Catch/CatchRuleset.cs index b1d0780739..16c9114c66 100644 --- a/osu.Game.Modes.Catch/CatchRuleset.cs +++ b/osu.Game.Modes.Catch/CatchRuleset.cs @@ -8,6 +8,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 { @@ -15,7 +16,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, PlayerInputManager input = null) => new CatchHitRenderer + { + Beatmap = beatmap, + InputManager = input, + }; public override IEnumerable GetModsFor(ModType type) { @@ -76,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/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..c9c3df8197 100644 --- a/osu.Game.Modes.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Modes.Catch/UI/CatchPlayfield.cs @@ -3,12 +3,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Modes.Catch.Objects; 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/ManiaRuleset.cs b/osu.Game.Modes.Mania/ManiaRuleset.cs index df8a48262e..d4b5ecad22 100644 --- a/osu.Game.Modes.Mania/ManiaRuleset.cs +++ b/osu.Game.Modes.Mania/ManiaRuleset.cs @@ -8,6 +8,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 { @@ -15,7 +16,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, PlayerInputManager input = null) => new ManiaHitRenderer + { + Beatmap = beatmap, + InputManager = input, + }; public override IEnumerable GetModsFor(ModType type) { @@ -92,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/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 e737771b59..ab3c231917 100644 --- a/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Modes.Mania/UI/ManiaPlayfield.cs @@ -3,13 +3,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Modes.Mania.Objects; using osu.Game.Modes.UI; using OpenTK; using OpenTK.Graphics; namespace osu.Game.Modes.Mania.UI { - public class ManiaPlayfield : Playfield + public class ManiaPlayfield : Playfield { public ManiaPlayfield(int columns) { diff --git a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs index 161a0c8cbc..e10fa91e6f 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -69,15 +69,11 @@ namespace osu.Game.Modes.Osu.Objects.Drawables Size = circle.DrawSize; } - private double hit50 = 150; - private double hit100 = 80; - private double hit300 = 30; - protected override void CheckJudgement(bool userTriggered) { if (!userTriggered) { - if (Judgement.TimeOffset > hit50) + if (Judgement.TimeOffset > HitObject.HitWindowFor(OsuScoreResult.Hit50)) Judgement.Result = HitResult.Miss; return; } @@ -86,16 +82,10 @@ namespace osu.Game.Modes.Osu.Objects.Drawables OsuJudgementInfo osuJudgement = (OsuJudgementInfo)Judgement; - if (hitOffset < hit50) + if (hitOffset < HitObject.HitWindowFor(OsuScoreResult.Hit50)) { Judgement.Result = HitResult.Hit; - - if (hitOffset < hit300) - osuJudgement.Score = OsuScoreResult.Hit300; - else if (hitOffset < hit100) - osuJudgement.Score = OsuScoreResult.Hit100; - else if (hitOffset < hit50) - 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 e2a75f72c1..4b436dd82f 100644 --- a/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -6,7 +6,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/Drawables/DrawableSlider.cs b/osu.Game.Modes.Osu/Objects/Drawables/DrawableSlider.cs index 4cd10a4b89..67acc7cc7e 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/Drawables/Pieces/CirclePiece.cs b/osu.Game.Modes.Osu/Objects/Drawables/Pieces/CirclePiece.cs index 23f878d491..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(128); + 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..ddcdae7be0 100644 --- a/osu.Game.Modes.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Modes.Osu/Objects/OsuHitObject.cs @@ -5,11 +5,19 @@ 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 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; @@ -22,10 +30,38 @@ 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; } + 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 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/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 629e664c1d..6c65e7fa37 100644 --- a/osu.Game.Modes.Osu/Objects/SliderCurve.cs +++ b/osu.Game.Modes.Osu/Objects/SliderCurve.cs @@ -185,10 +185,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) { diff --git a/osu.Game.Modes.Osu/OsuAutoReplay.cs b/osu.Game.Modes.Osu/OsuAutoReplay.cs new file mode 100644 index 0000000000..a0fa0904af --- /dev/null +++ b/osu.Game.Modes.Osu/OsuAutoReplay.cs @@ -0,0 +1,298 @@ +// 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 + { + static readonly Vector2 spinner_centre = new Vector2(256, 192); + + const float spin_radius = 50; + + 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 int findInsertionIndex(LegacyReplayFrame frame) + { + int index = Frames.BinarySearch(frame, replayFrameComparer); + + if (index < 0) + { + index = ~index; + } + else + { + // Go to the first index which is actually bigger + while (index < Frames.Count && frame.Time == Frames[index].Time) + { + ++index; + } + } + + return index; + } + + private void addFrameToReplay(LegacyReplayFrame frame) => Frames.Insert(findInsertionIndex(frame), frame); + + 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() + { + int buttonIndex = 0; + + bool delayedMovements = false;// ModManager.CheckActive(Mods.Relax2); + EasingTypes preferredEasing = delayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out; + + 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); + + // 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 - 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 + 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 - 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 + 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 - 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 + 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)); + } + } + + + 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 = spinner_centre - targetPosition; + + float differenceLength = difference.Length; + float newLength = (float)Math.Sqrt(differenceLength * differenceLength - spin_radius * spin_radius); + + if (differenceLength > spin_radius) + { + float angle = (float)Math.Asin(spin_radius / 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 = spinner_centre - difference * (spin_radius / difference.Length); + } + else + { + targetPosition = spinner_centre + new Vector2(0, -spin_radius); + } + } + + + // 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(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 > 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 + for (double time = lastFrame.Time + frameDelay; time < h.StartTime; time += frameDelay) + { + Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPosition, lastFrame.Time, h.StartTime, easing); + addFrameToReplay(new LegacyReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState)); + } + + buttonIndex = 0; + } + else + { + buttonIndex++; + } + } + + LegacyButtonState button = buttonIndex % 2 == 0 ? LegacyButtonState.Left1 : LegacyButtonState.Right1; + + 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(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]; + var 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(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(newFrame); + + // We add intermediate frames for spinning / following a slider here. + if (h is Spinner) + { + Vector2 difference = targetPosition - spinner_centre; + + 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 = 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 = spinner_centre + circlePosition(t / 20 + angle, spin_radius); + + addFrameToReplay(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; + + 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)); + } + + // 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(endFrame); + } + + //Player.currentScore.Replay = InputManager.ReplayScore.Replay; + //Player.currentScore.PlayerName = "osu!"; + } + } +} diff --git a/osu.Game.Modes.Osu/OsuRuleset.cs b/osu.Game.Modes.Osu/OsuRuleset.cs index 11a67bf83b..b58dc70b16 100644 --- a/osu.Game.Modes.Osu/OsuRuleset.cs +++ b/osu.Game.Modes.Osu/OsuRuleset.cs @@ -9,6 +9,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 { @@ -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, PlayerInputManager input = null) => new OsuHitRenderer + { + Beatmap = beatmap, + InputManager = input + }; public override IEnumerable GetBeatmapStatistics(WorkingBeatmap beatmap) => new[] { @@ -96,10 +101,17 @@ 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 CreateAutoplayScore(Beatmap beatmap) + { + var score = CreateScoreProcessor().GetScore(); + score.Replay = new OsuAutoReplay(beatmap); + return score; + } + protected override PlayMode PlayMode => PlayMode.Osu; } } diff --git a/osu.Game.Modes.Osu/OsuScoreProcessor.cs b/osu.Game.Modes.Osu/OsuScoreProcessor.cs index 85ecdf8304..7bc82c3c8b 100644 --- a/osu.Game.Modes.Osu/OsuScoreProcessor.cs +++ b/osu.Game.Modes.Osu/OsuScoreProcessor.cs @@ -8,7 +8,7 @@ namespace osu.Game.Modes.Osu { internal class OsuScoreProcessor : ScoreProcessor { - public OsuScoreProcessor(int hitObjectCount) + public OsuScoreProcessor(int hitObjectCount = 0) : base(hitObjectCount) { Health.Value = 1; diff --git a/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs b/osu.Game.Modes.Osu/UI/OsuHitRenderer.cs index 9d89d1ce7a..4801862a91 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) { var circle = h as HitCircle; if (circle != null) diff --git a/osu.Game.Modes.Osu/UI/OsuPlayfield.cs b/osu.Game.Modes.Osu/UI/OsuPlayfield.cs index 20164060fe..26cda012e5 100644 --- a/osu.Game.Modes.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Modes.Osu/UI/OsuPlayfield.cs @@ -10,10 +10,12 @@ 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; +using OpenTK.Graphics; namespace osu.Game.Modes.Osu.UI { - public class OsuPlayfield : Playfield + public class OsuPlayfield : Playfield { private Container approachCircles; private Container judgementLayer; @@ -53,11 +55,18 @@ namespace osu.Game.Modes.Osu.UI { RelativeSizeAxes = Axes.Both, Depth = -1, - } + }, }); } - public override void Add(DrawableHitObject h) + 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; IDrawableHitObjectWithProxiedApproach c = h as IDrawableHitObjectWithProxiedApproach; @@ -78,9 +87,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.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.Taiko/TaikoRuleset.cs b/osu.Game.Modes.Taiko/TaikoRuleset.cs index 71b747f895..73e65a2734 100644 --- a/osu.Game.Modes.Taiko/TaikoRuleset.cs +++ b/osu.Game.Modes.Taiko/TaikoRuleset.cs @@ -8,6 +8,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 { @@ -15,7 +16,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, PlayerInputManager input = null) => new TaikoHitRenderer + { + Beatmap = beatmap, + InputManager = input, + }; public override IEnumerable GetModsFor(ModType type) { @@ -76,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/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..2fcfce4fa3 100644 --- a/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Modes.Taiko/UI/TaikoPlayfield.cs @@ -5,13 +5,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +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.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 13eb069e2b..ae936f3f49 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/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 9393c2b0e9..1d4047ea45 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -2,12 +2,15 @@ // 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; 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 +20,16 @@ namespace osu.Game.Beatmaps public readonly BeatmapSetInfo BeatmapSetInfo; + /// + /// 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 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/Database/BeatmapDatabase.cs b/osu.Game/Database/BeatmapDatabase.cs index 4bf49c289e..871251b882 100644 --- a/osu.Game/Database/BeatmapDatabase.cs +++ b/osu.Game/Database/BeatmapDatabase.cs @@ -6,7 +6,7 @@ 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; using osu.Game.Beatmaps; @@ -26,14 +26,14 @@ namespace osu.Game.Database public event Action BeatmapSetRemoved; // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) - 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) { @@ -152,7 +152,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); } @@ -163,7 +163,7 @@ namespace osu.Game.Database /// Location on disk public void Import(string path) { - Import(new [] { path }); + Import(new[] { path }); } /// @@ -182,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)) @@ -217,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 214401bb76..28c9e51d2a 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/Graphics/Cursor/CursorTrail.cs b/osu.Game/Graphics/Cursor/CursorTrail.cs index 89f486c37c..09d1193661 100644 --- a/osu.Game/Graphics/Cursor/CursorTrail.cs +++ b/osu.Game/Graphics/Cursor/CursorTrail.cs @@ -31,7 +31,7 @@ namespace osu.Game.Graphics.Cursor private double timeOffset; private float time; - + private TrailDrawNodeSharedData trailDrawNodeSharedData = new TrailDrawNodeSharedData(); private const int max_sprites = 2048; @@ -76,6 +76,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(); diff --git a/osu.Game/Graphics/Cursor/OsuCursorContainer.cs b/osu.Game/Graphics/Cursor/OsuCursorContainer.cs index 9177557139..8b71182263 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 { - internal 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); } - private 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/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 c4f4f7a712..0000000000 --- a/osu.Game/IPC/BeatmapImporter.cs +++ /dev/null @@ -1,53 +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.Diagnostics; -using System.Threading.Tasks; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Game.Database; - -namespace osu.Game.IPC -{ - public class BeatmapImporter : IDisposable - { - 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).ContinueWith(t => Logger.Error(t.Exception, @"error during async import"), TaskContinuationOptions.OnlyOnFaulted); - } - - public void Dispose() - { - throw new NotImplementedException(); - } - } - - public class BeatmapImportMessage - { - public string Path; - } -} 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/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs new file mode 100644 index 0000000000..76e038048c --- /dev/null +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Input.Handlers; +using osu.Framework.Platform; +using OpenTK; + +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 should not advance time as we do not have enough 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/LegacyReplay.cs b/osu.Game/Modes/LegacyReplay.cs new file mode 100644 index 0000000000..0c9fb6ffca --- /dev/null +++ b/osu.Game/Modes/LegacyReplay.cs @@ -0,0 +1,274 @@ +// 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 + { + protected List Frames = new List(); + + protected LegacyReplay() + { + + } + + 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; + + public LegacyReplayFrame CurrentFrame => !hasFrames ? null : replayContent[currentFrameIndex]; + 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 advanceFrame() + { + int newFrame = nextFrameIndex; + + //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; + + 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.0 / 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 should not advance time as we do not have enough 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) && 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. + 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/Mod.cs b/osu.Game/Modes/Mod.cs index d3286cd643..16c39ce044 100644 --- a/osu.Game/Modes/Mod.cs +++ b/osu.Game/Modes/Mod.cs @@ -3,6 +3,7 @@ using System; using osu.Game.Graphics; +using osu.Game.Screens.Play; namespace osu.Game.Modes { @@ -40,6 +41,12 @@ namespace osu.Game.Modes /// The mods this mod cannot be enabled with. /// public virtual Type[] IncompatibleMods => new Type[] { }; + + /// + /// Direct access to the Player before load has run. + /// + /// + public virtual void PlayerLoading(Player player) { } } public class MultiMod : Mod @@ -144,6 +151,12 @@ namespace osu.Game.Modes public override string Description => "Watch a perfect automated play through the song"; public override double ScoreMultiplier => 0; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModPerfect) }; + + 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/Modes/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Modes/Objects/Drawables/DrawableHitObject.cs index 38ed2ca9d4..ac18ae5a04 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 protected abstract JudgementInfo CreateJudgementInfo(); - public HitObject HitObject; - - protected DrawableHitObject(HitObject hitObject) - { - HitObject = hitObject; - } + protected abstract void UpdateState(ArmedState state); private ArmedState state; public ArmedState State @@ -52,23 +45,11 @@ namespace osu.Game.Modes.Objects.Drawables } } - private SampleChannel sample; - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - SampleType type = HitObject.Sample?.Type ?? SampleType.None; - if (type == SampleType.None) - type = SampleType.Normal; - string hitType = 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() @@ -84,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; } /// @@ -145,7 +126,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/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/Ruleset.cs b/osu.Game/Modes/Ruleset.cs index 8fb35dc1d4..e145e60b97 100644 --- a/osu.Game/Modes/Ruleset.cs +++ b/osu.Game/Modes/Ruleset.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Concurrent; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Screens.Play; namespace osu.Game.Modes { @@ -28,9 +29,9 @@ 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); + public abstract HitRenderer CreateHitRendererWith(Beatmap beatmap, PlayerInputManager input = null); public abstract HitObjectParser CreateHitObjectParser(); @@ -42,6 +43,8 @@ namespace osu.Game.Modes public virtual FontAwesome Icon => FontAwesome.fa_question_circle; + public virtual Score CreateAutoplayScore(Beatmap beatmap) => null; + public static Ruleset GetRuleset(PlayMode mode) { Type type; @@ -51,5 +54,6 @@ namespace osu.Game.Modes return Activator.CreateInstance(type) as Ruleset; } + } } diff --git a/osu.Game/Modes/Score.cs b/osu.Game/Modes/Score.cs index 8da09cb974..1a761bea5d 100644 --- a/osu.Game/Modes/Score.cs +++ b/osu.Game/Modes/Score.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Database; + namespace osu.Game.Modes { public class Score @@ -10,5 +12,8 @@ namespace osu.Game.Modes public double Combo { get; set; } public double MaxCombo { get; set; } public double Health { get; set; } + + public Replay Replay; + public BeatmapInfo Beatmap; } } diff --git a/osu.Game/Modes/UI/HitRenderer.cs b/osu.Game/Modes/UI/HitRenderer.cs index 5793a4cd43..cf5c2460d2 100644 --- a/osu.Game/Modes/UI/HitRenderer.cs +++ b/osu.Game/Modes/UI/HitRenderer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; 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 { @@ -19,24 +20,28 @@ namespace osu.Game.Modes.UI public event Action OnAllJudged; + public abstract bool AllObjectsJudged { get; } + protected void TriggerOnJudgement(JudgementInfo j) { OnJudgement?.Invoke(j); 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 PlayerInputManager 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 { @@ -48,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); protected HitRenderer() { @@ -62,10 +67,10 @@ namespace osu.Game.Modes.UI [BackgroundDependencyLoader] private void load() { - Children = new Drawable[] - { - Playfield = CreatePlayfield() - }; + Playfield = CreatePlayfield(); + Playfield.InputManager = InputManager; + + Add(Playfield); loadObjects(); } @@ -73,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; @@ -86,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 212c771efb..502e072603 100644 --- a/osu.Game/Modes/UI/Playfield.cs +++ b/osu.Game/Modes/UI/Playfield.cs @@ -1,36 +1,72 @@ // 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.Game.Modes.Objects; using osu.Game.Modes.Objects.Drawables; +using osu.Game.Screens.Play; namespace osu.Game.Modes.UI { - public abstract class Playfield : Container + public abstract class Playfield : Container + where T : HitObject { - public HitObjectContainer HitObjects; + 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; protected override Container Content { get; } - protected Playfield() + 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 + Add(HitObjects = new HitObjectContainer> { RelativeSizeAxes = Axes.Both, }); } + /// + /// An optional inputManager to provide interactivity etc. + /// + public PlayerInputManager 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/OsuGame.cs b/osu.Game/OsuGame.cs index db6f266bf9..79ffcfcadf 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -23,9 +23,11 @@ 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.Framework.Threading; +using osu.Game.Graphics; using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Play; namespace osu.Game { @@ -81,7 +83,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); @@ -89,9 +91,41 @@ namespace osu.Game PlayMode = LocalConfig.GetBindable(OsuConfig.PlayMode); } - protected async void ImportBeatmapsAsync(IEnumerable paths) + private ScheduledDelegate scoreLoad; + + protected void LoadScore(Score s) { - await Task.Run(() => BeatmapDatabase.Import(paths)); + scoreLoad?.Cancel(); + + var menu = intro.ChildScreen; + + if (menu == null) + { + scoreLoad = Schedule(() => LoadScore(s)); + return; + } + + if (!menu.IsCurrentScreen) + { + menu.MakeCurrent(); + Delay(500); + scoreLoad = 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() diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index db6fa3956f..db792d4a7e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -28,6 +28,8 @@ namespace osu.Game protected BeatmapDatabase BeatmapDatabase; + protected ScoreDatabase ScoreDatabase; + protected override string MainResourceFile => @"osu.Game.Resources.dll"; public APIAccess API; @@ -79,6 +81,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/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 80af1e70e5..d722eb356a 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(); diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 34cf10fe25..747b46aecc 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -136,6 +136,8 @@ namespace osu.Game.Overlays FadeIn(100, EasingTypes.OutQuint); contentContainer.MoveToY(0, APPEAR_DURATION, EasingTypes.OutQuint); + + FadeIn(100, EasingTypes.OutQuint); } protected override void PopOut() @@ -147,6 +149,8 @@ namespace osu.Game.Overlays foreach (var w in wavesContainer.Children) w.State = Visibility.Hidden; + + FadeOut(DISAPPEAR_DURATION, EasingTypes.InQuint); } protected override void UpdateAfterChildren() diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 87879c9e97..f1206034b7 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; @@ -19,26 +17,24 @@ 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; -using osu.Framework.Logging; using osu.Framework.Input; +using osu.Framework.Logging; +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; public BeatmapInfo BeatmapInfo; - public PlayMode PreferredPlayMode; - public bool IsPaused { get; private set; } public int RestartCount; @@ -49,6 +45,7 @@ namespace osu.Game.Screens.Play private bool canPause => Time.Current >= lastPauseActionTime + pauseCooldown; private IAdjustableClock sourceClock; + private IFrameBasedClock interpolatedSourceClock; private Ruleset ruleset; @@ -59,11 +56,21 @@ 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) { + 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); @@ -93,24 +100,14 @@ namespace osu.Game.Screens.Play } sourceClock = (IAdjustableClock)track ?? new StopwatchClock(); + interpolatedSourceClock = new InterpolatingFramedClock(sourceClock); Schedule(() => { 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; - } - - 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)); @@ -127,7 +124,10 @@ namespace osu.Game.Screens.Play OnQuit = Exit }; - hitRenderer = ruleset.CreateHitRendererWith(beatmap); + hitRenderer = ruleset.CreateHitRendererWith(beatmap, new PlayerInputManager + { + ReplayInputHandler = ReplayInputHandler + }); //bind HitRenderer to ScoreProcessor and ourselves (for a pass situation) hitRenderer.OnJudgement += scoreProcessor.AddJudgement; @@ -136,18 +136,19 @@ 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[] { - playerInputManager = new PlayerInputManager + new Container { - Clock = new InterpolatingFramedClock(sourceClock), + RelativeSizeAxes = Axes.Both, + Clock = interpolatedSourceClock, Children = new Drawable[] { hitRenderer, - skipButton = new SkipButton { Alpha = 0 }, + skipButton = new SkipButton + { + Alpha = 0 + }, } }, scoreOverlay, @@ -227,8 +228,8 @@ namespace osu.Game.Screens.Play if (!Push(newPlayer)) { - // Error(?) - } + // Error(?) + } }); } @@ -295,6 +296,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 @@ -320,6 +323,8 @@ namespace osu.Game.Screens.Play private Bindable mouseWheelDisabled; + public ReplayInputHandler ReplayInputHandler; + protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value && !IsPaused; } } \ No newline at end of file diff --git a/osu.Game/Screens/Play/PlayerInputManager.cs b/osu.Game/Screens/Play/PlayerInputManager.cs index a2ea4ee341..6d669e5169 100644 --- a/osu.Game/Screens/Play/PlayerInputManager.cs +++ b/osu.Game/Screens/Play/PlayerInputManager.cs @@ -1,40 +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.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 { - internal class PlayerInputManager : UserInputManager + public class PlayerInputManager : PassThroughInputManager { private bool leftViaKeyboard; private bool rightViaKeyboard; private Bindable mouseDisabled; + private ManualClock clock = new ManualClock(); + private IFrameBasedClock parentClock; + + private ReplayInputHandler replayInputHandler; + public ReplayInputHandler ReplayInputHandler + { + get { return replayInputHandler; } + set + { + if (replayInputHandler != null) RemoveHandler(replayInputHandler); + + replayInputHandler = value; + UseParentState = replayInputHandler == null; + + if (replayInputHandler != null) + { + replayInputHandler.ToScreenSpace = ToScreenSpace; + AddHandler(replayInputHandler); + } + } + } + + 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) { @@ -48,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/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 43ad7e1657..c93f61fd6e 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 present before push. })).LoadAsync(Game, l => Push(player)); } }, @@ -336,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); 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 a6eb600824..61c31a6cee 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 @@ + @@ -79,9 +84,16 @@ + + + + + + + @@ -188,7 +200,7 @@ - + @@ -326,6 +338,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 +