diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index ff930b07a3..2bff304fba 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,9 +1,11 @@ -osu!lazer is currently in early stages of development and is not yet ready for end users. Please avoid creating issues or bugs if you do not personally intend to fix them. Some acceptable topics include: +osu!lazer is currently still under heavy development! +Please ensure that you are making an issue for one of the following: + +- A bug with currently implemented features (not features that don't exist) +- A feature you are considering adding, so we can collaborate on feedback and design. - Discussions about technical design decisions -- Bugs that you have found and are personally willing and able to fix -- TODO lists of smaller tasks around larger features - -Basically, issues are not a place for you to get help. They are a place for developers to collaborate on the game. If your issue qualifies, replace this text with a detailed description of your issue with as much relevant information as you can provide. + +Screenshots and log files are highly welcomed. \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index b86082334d..e63f6ea55c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,6 +20,10 @@ build: project: osu.sln parallel: true verbosity: minimal +test: + assemblies: + only: + - 'osu.Desktop\**\*.dll' after_build: - cmd: inspectcode --o="inspectcodereport.xml" --projects:osu.Game* --caches-home="inspectcode" osu.sln > NUL - cmd: NVika parsereport "inspectcodereport.xml" --treatwarningsaserrors \ No newline at end of file diff --git a/osu-framework b/osu-framework index 16a4bef775..59004b46f2 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 16a4bef775a49166f38faa6e952d83d8823fe3e0 +Subproject commit 59004b46f2c96ac02fec712e66f9f96fe252f2fa diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 048fe93c11..d036a6822c 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Runtime; using osu.Framework; using osu.Framework.Platform; using osu.Game.IPC; @@ -15,6 +16,9 @@ namespace osu.Desktop [STAThread] public static int Main(string[] args) { + if (!RuntimeInfo.IsMono) + useMulticoreJit(); + // Back up the cwd before DesktopGameHost changes it var cwd = Environment.CurrentDirectory; @@ -44,8 +48,16 @@ namespace osu.Desktop break; } } + return 0; } } + + private static void useMulticoreJit() + { + var directory = Directory.CreateDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Profiles")); + ProfileOptimization.SetProfileRoot(directory.FullName); + ProfileOptimization.StartProfile("Startup.Profile"); + } } } diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 5e70239c7c..4dbe65b3ce 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -10,6 +10,8 @@ using osu.Game.Rulesets.UI; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Catch.Replays; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Catch { @@ -99,7 +101,9 @@ namespace osu.Game.Rulesets.Catch public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap); - public override int LegacyID => 2; + public override int? LegacyID => 2; + + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); public CatchRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index be1e360fce..a3e5aba2db 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Objects { StartTime = lastTickTime, ComboColour = ComboColour, - X = Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH, + X = X + Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH, Samples = new List(Samples.Select(s => new SampleInfo { Bank = s.Bank, @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Objects { StartTime = spanStartTime + t, ComboColour = ComboColour, - X = Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, + X = X + Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, Samples = new List(Samples.Select(s => new SampleInfo { Bank = s.Bank, @@ -120,14 +120,14 @@ namespace osu.Game.Rulesets.Catch.Objects Samples = Samples, ComboColour = ComboColour, StartTime = spanStartTime + spanDuration, - X = Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH + X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH }); } } public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; - public float EndX => Curve.PositionAt(this.ProgressAt(1)).X / CatchPlayfield.BASE_WIDTH; + public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; public double Duration => EndTime - StartTime; diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index f8ca75fae9..f1503a14ee 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.Replays } else if (h.HyperDash) { - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable, lastPosition, ReplayButtonState.Right1)); + Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable, lastPosition)); Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); } else if (dashRequired) @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Replays float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable); //dash movement - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + 1, lastPosition, ReplayButtonState.Left1)); + Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + 1, lastPosition, true)); Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition)); Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); } @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Replays { double timeBefore = positionChange / movement_speed; - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeBefore, lastPosition, ReplayButtonState.Right1)); + Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeBefore, lastPosition)); Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); } diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs index 2f296a2504..9c9b06fcea 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs @@ -3,37 +3,51 @@ using System.Collections.Generic; using osu.Framework.Input; +using osu.Framework.MathUtils; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Catch.Replays { - public class CatchFramedReplayInputHandler : FramedReplayInputHandler + public class CatchFramedReplayInputHandler : FramedReplayInputHandler { public CatchFramedReplayInputHandler(Replay replay) : base(replay) { } + protected override bool IsImportant(CatchReplayFrame frame) => frame.Position > 0; + + protected float? Position + { + get + { + if (!HasFrames) + return null; + + return Interpolation.ValueAt(CurrentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time); + } + } + public override List GetPendingStates() { if (!Position.HasValue) return new List(); - var action = new List(); + var actions = new List(); - if (CurrentFrame.ButtonState == ReplayButtonState.Left1) - action.Add(CatchAction.Dash); + if (CurrentFrame.Dashing) + actions.Add(CatchAction.Dash); - if (Position.Value.X > CurrentFrame.Position.X) - action.Add(CatchAction.MoveRight); - else if (Position.Value.X < CurrentFrame.Position.X) - action.Add(CatchAction.MoveLeft); + if (Position.Value > CurrentFrame.Position) + actions.Add(CatchAction.MoveRight); + else if (Position.Value < CurrentFrame.Position) + actions.Add(CatchAction.MoveLeft); return new List { new CatchReplayState { - PressedActions = action, - CatcherX = Position.Value.X + PressedActions = actions, + CatcherX = Position.Value }, }; } diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index 0194fc93a4..b444b0d7ba 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -1,17 +1,34 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Catch.Replays { - public class CatchReplayFrame : ReplayFrame + public class CatchReplayFrame : ReplayFrame, IConvertibleReplayFrame { - public override bool IsImportant => MouseX > 0; + public float Position; + public bool Dashing; - public CatchReplayFrame(double time, float? x = null, ReplayButtonState button = ReplayButtonState.None) - : base(time, x ?? -1, null, button) + public CatchReplayFrame() { } + + public CatchReplayFrame(double time, float? position = null, bool dashing = false) + : base(time) + { + Position = position ?? -1; + Dashing = dashing; + } + + public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + { + Position = legacyFrame.Position.X / CatchPlayfield.BASE_WIDTH; + Dashing = legacyFrame.ButtonState == ReplayButtonState.Left1; + } } } diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json new file mode 100644 index 0000000000..9357d3b75c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -0,0 +1,957 @@ +{ + "Mappings": [{ + "StartTime": 500.0, + "Objects": [{ + "StartTime": 500.0, + "Position": 96.0 + }, { + "StartTime": 562.0, + "Position": 100.84 + }, { + "StartTime": 625.0, + "Position": 125.0 + }, { + "StartTime": 687.0, + "Position": 152.84 + }, { + "StartTime": 750.0, + "Position": 191.0 + }, { + "StartTime": 812.0, + "Position": 212.84 + }, { + "StartTime": 875.0, + "Position": 217.0 + }, { + "StartTime": 937.0, + "Position": 234.84 + }, { + "StartTime": 1000.0, + "Position": 256.0 + }, { + "StartTime": 1062.0, + "Position": 267.84 + }, { + "StartTime": 1125.0, + "Position": 284.0 + }, { + "StartTime": 1187.0, + "Position": 311.84 + }, { + "StartTime": 1250.0, + "Position": 350.0 + }, { + "StartTime": 1312.0, + "Position": 359.84 + }, { + "StartTime": 1375.0, + "Position": 367.0 + }, { + "StartTime": 1437.0, + "Position": 400.84 + }, { + "StartTime": 1500.0, + "Position": 416.0 + }, { + "StartTime": 1562.0, + "Position": 377.159973 + }, { + "StartTime": 1625.0, + "Position": 367.0 + }, { + "StartTime": 1687.0, + "Position": 374.159973 + }, { + "StartTime": 1750.0, + "Position": 353.0 + }, { + "StartTime": 1812.0, + "Position": 329.159973 + }, { + "StartTime": 1875.0, + "Position": 288.0 + }, { + "StartTime": 1937.0, + "Position": 259.159973 + }, { + "StartTime": 2000.0, + "Position": 256.0 + }, { + "StartTime": 2058.0, + "Position": 232.44 + }, { + "StartTime": 2116.0, + "Position": 222.879974 + }, { + "StartTime": 2174.0, + "Position": 185.319992 + }, { + "StartTime": 2232.0, + "Position": 177.76001 + }, { + "StartTime": 2290.0, + "Position": 162.200012 + }, { + "StartTime": 2348.0, + "Position": 158.639984 + }, { + "StartTime": 2406.0, + "Position": 111.079994 + }, { + "StartTime": 2500.0, + "Position": 96.0 + }] + }, { + "StartTime": 3000.0, + "Objects": [{ + "StartTime": 3000.0, + "Position": 18.0 + }, { + "StartTime": 3062.0, + "Position": 482.0 + }, { + "StartTime": 3125.0, + "Position": 243.0 + }, { + "StartTime": 3187.0, + "Position": 332.0 + }, { + "StartTime": 3250.0, + "Position": 477.0 + }, { + "StartTime": 3312.0, + "Position": 376.0 + }, { + "StartTime": 3375.0, + "Position": 104.0 + }, { + "StartTime": 3437.0, + "Position": 156.0 + }, { + "StartTime": 3500.0, + "Position": 135.0 + }, { + "StartTime": 3562.0, + "Position": 256.0 + }, { + "StartTime": 3625.0, + "Position": 360.0 + }, { + "StartTime": 3687.0, + "Position": 199.0 + }, { + "StartTime": 3750.0, + "Position": 239.0 + }, { + "StartTime": 3812.0, + "Position": 326.0 + }, { + "StartTime": 3875.0, + "Position": 393.0 + }, { + "StartTime": 3937.0, + "Position": 470.0 + }, { + "StartTime": 4000.0, + "Position": 136.0 + }] + }, { + "StartTime": 4500.0, + "Objects": [{ + "StartTime": 4500.0, + "Position": 317.0 + }, { + "StartTime": 4562.0, + "Position": 354.0 + }, { + "StartTime": 4625.0, + "Position": 414.0 + }, { + "StartTime": 4687.0, + "Position": 39.0 + }, { + "StartTime": 4750.0, + "Position": 172.0 + }, { + "StartTime": 4812.0, + "Position": 479.0 + }, { + "StartTime": 4875.0, + "Position": 18.0 + }, { + "StartTime": 4937.0, + "Position": 151.0 + }, { + "StartTime": 5000.0, + "Position": 342.0 + }, { + "StartTime": 5062.0, + "Position": 400.0 + }, { + "StartTime": 5125.0, + "Position": 420.0 + }, { + "StartTime": 5187.0, + "Position": 90.0 + }, { + "StartTime": 5250.0, + "Position": 220.0 + }, { + "StartTime": 5312.0, + "Position": 80.0 + }, { + "StartTime": 5375.0, + "Position": 421.0 + }, { + "StartTime": 5437.0, + "Position": 473.0 + }, { + "StartTime": 5500.0, + "Position": 97.0 + }] + }, { + "StartTime": 6000.0, + "Objects": [{ + "StartTime": 6000.0, + "Position": 105.0 + }, { + "StartTime": 6062.0, + "Position": 249.0 + }, { + "StartTime": 6125.0, + "Position": 163.0 + }, { + "StartTime": 6187.0, + "Position": 194.0 + }, { + "StartTime": 6250.0, + "Position": 106.0 + }, { + "StartTime": 6312.0, + "Position": 212.0 + }, { + "StartTime": 6375.0, + "Position": 257.0 + }, { + "StartTime": 6437.0, + "Position": 461.0 + }, { + "StartTime": 6500.0, + "Position": 79.0 + }] + }, { + "StartTime": 7000.0, + "Objects": [{ + "StartTime": 7000.0, + "Position": 256.0 + }, { + "StartTime": 7062.0, + "Position": 294.84 + }, { + "StartTime": 7125.0, + "Position": 279.0 + }, { + "StartTime": 7187.0, + "Position": 309.84 + }, { + "StartTime": 7250.0, + "Position": 336.0 + }, { + "StartTime": 7312.0, + "Position": 322.16 + }, { + "StartTime": 7375.0, + "Position": 308.0 + }, { + "StartTime": 7437.0, + "Position": 263.16 + }, { + "StartTime": 7500.0, + "Position": 256.0 + }, { + "StartTime": 7562.0, + "Position": 261.84 + }, { + "StartTime": 7625.0, + "Position": 277.0 + }, { + "StartTime": 7687.0, + "Position": 318.84 + }, { + "StartTime": 7750.0, + "Position": 336.0 + }, { + "StartTime": 7803.0, + "Position": 305.04 + }, { + "StartTime": 7857.0, + "Position": 307.76 + }, { + "StartTime": 7910.0, + "Position": 297.8 + }, { + "StartTime": 8000.0, + "Position": 256.0 + }] + }, { + "StartTime": 8500.0, + "Objects": [{ + "StartTime": 8500.0, + "Position": 32.0 + }, { + "StartTime": 8562.0, + "Position": 22.8515015 + }, { + "StartTime": 8625.0, + "Position": 28.5659637 + }, { + "StartTime": 8687.0, + "Position": 50.3433228 + }, { + "StartTime": 8750.0, + "Position": 56.58974 + }, { + "StartTime": 8812.0, + "Position": 64.23422 + }, { + "StartTime": 8875.0, + "Position": 67.7117844 + }, { + "StartTime": 8937.0, + "Position": 90.52607 + }, { + "StartTime": 9000.0, + "Position": 101.81015 + }, { + "StartTime": 9062.0, + "Position": 113.478188 + }, { + "StartTime": 9125.0, + "Position": 159.414444 + }, { + "StartTime": 9187.0, + "Position": 155.1861 + }, { + "StartTime": 9250.0, + "Position": 179.600418 + }, { + "StartTime": 9312.0, + "Position": 212.293015 + }, { + "StartTime": 9375.0, + "Position": 197.2076 + }, { + "StartTime": 9437.0, + "Position": 243.438324 + }, { + "StartTime": 9500.0, + "Position": 237.2304 + }, { + "StartTime": 9562.0, + "Position": 241.253983 + }, { + "StartTime": 9625.0, + "Position": 258.950623 + }, { + "StartTime": 9687.0, + "Position": 253.3786 + }, { + "StartTime": 9750.0, + "Position": 270.8865 + }, { + "StartTime": 9812.0, + "Position": 244.38974 + }, { + "StartTime": 9875.0, + "Position": 242.701874 + }, { + "StartTime": 9937.0, + "Position": 256.2331 + }, { + "StartTime": 10000.0, + "Position": 270.339874 + }, { + "StartTime": 10062.0, + "Position": 275.9349 + }, { + "StartTime": 10125.0, + "Position": 297.2969 + }, { + "StartTime": 10187.0, + "Position": 307.834137 + }, { + "StartTime": 10250.0, + "Position": 321.6449 + }, { + "StartTime": 10312.0, + "Position": 357.746338 + }, { + "StartTime": 10375.0, + "Position": 358.21875 + }, { + "StartTime": 10437.0, + "Position": 394.943 + }, { + "StartTime": 10500.0, + "Position": 401.0588 + }, { + "StartTime": 10558.0, + "Position": 418.21347 + }, { + "StartTime": 10616.0, + "Position": 424.6034 + }, { + "StartTime": 10674.0, + "Position": 455.835754 + }, { + "StartTime": 10732.0, + "Position": 477.5042 + }, { + "StartTime": 10790.0, + "Position": 476.290955 + }, { + "StartTime": 10848.0, + "Position": 470.943237 + }, { + "StartTime": 10906.0, + "Position": 503.3372 + }, { + "StartTime": 10999.0, + "Position": 508.166229 + }] + }, { + "StartTime": 11500.0, + "Objects": [{ + "StartTime": 11500.0, + "Position": 321.0 + }, { + "StartTime": 11562.0, + "Position": 17.0 + }, { + "StartTime": 11625.0, + "Position": 173.0 + }, { + "StartTime": 11687.0, + "Position": 170.0 + }, { + "StartTime": 11750.0, + "Position": 447.0 + }, { + "StartTime": 11812.0, + "Position": 218.0 + }, { + "StartTime": 11875.0, + "Position": 394.0 + }, { + "StartTime": 11937.0, + "Position": 46.0 + }, { + "StartTime": 12000.0, + "Position": 480.0 + }] + }, { + "StartTime": 12500.0, + "Objects": [{ + "StartTime": 12500.0, + "Position": 512.0 + }, { + "StartTime": 12562.0, + "Position": 491.3132 + }, { + "StartTime": 12625.0, + "Position": 484.3089 + }, { + "StartTime": 12687.0, + "Position": 454.6221 + }, { + "StartTime": 12750.0, + "Position": 433.617767 + }, { + "StartTime": 12812.0, + "Position": 399.930969 + }, { + "StartTime": 12875.0, + "Position": 395.926666 + }, { + "StartTime": 12937.0, + "Position": 361.239868 + }, { + "StartTime": 13000.0, + "Position": 353.235535 + }, { + "StartTime": 13062.0, + "Position": 314.548767 + }, { + "StartTime": 13125.0, + "Position": 315.544434 + }, { + "StartTime": 13187.0, + "Position": 288.857635 + }, { + "StartTime": 13250.0, + "Position": 254.853333 + }, { + "StartTime": 13312.0, + "Position": 239.166534 + }, { + "StartTime": 13375.0, + "Position": 240.1622 + }, { + "StartTime": 13437.0, + "Position": 212.4754 + }, { + "StartTime": 13500.0, + "Position": 194.471069 + }, { + "StartTime": 13562.0, + "Position": 161.784271 + }, { + "StartTime": 13625.0, + "Position": 145.779968 + }, { + "StartTime": 13687.0, + "Position": 129.09314 + }, { + "StartTime": 13750.0, + "Position": 104.088837 + }, { + "StartTime": 13812.0, + "Position": 95.40204 + }, { + "StartTime": 13875.0, + "Position": 61.3977356 + }, { + "StartTime": 13937.0, + "Position": 56.710907 + }, { + "StartTime": 14000.0, + "Position": 35.7066345 + }, { + "StartTime": 14062.0, + "Position": 5.019806 + }, { + "StartTime": 14125.0, + "Position": 0.0 + }, { + "StartTime": 14187.0, + "Position": 39.7696266 + }, { + "StartTime": 14250.0, + "Position": 23.0119171 + }, { + "StartTime": 14312.0, + "Position": 75.94882 + }, { + "StartTime": 14375.0, + "Position": 98.19112 + }, { + "StartTime": 14437.0, + "Position": 82.12803 + }, { + "StartTime": 14500.0, + "Position": 118.370323 + }, { + "StartTime": 14562.0, + "Position": 149.307236 + }, { + "StartTime": 14625.0, + "Position": 168.549515 + }, { + "StartTime": 14687.0, + "Position": 190.486435 + }, { + "StartTime": 14750.0, + "Position": 186.728714 + }, { + "StartTime": 14812.0, + "Position": 199.665634 + }, { + "StartTime": 14875.0, + "Position": 228.907928 + }, { + "StartTime": 14937.0, + "Position": 264.844849 + }, { + "StartTime": 15000.0, + "Position": 271.087128 + }, { + "StartTime": 15062.0, + "Position": 290.024017 + }, { + "StartTime": 15125.0, + "Position": 302.266327 + }, { + "StartTime": 15187.0, + "Position": 344.203247 + }, { + "StartTime": 15250.0, + "Position": 356.445526 + }, { + "StartTime": 15312.0, + "Position": 359.382446 + }, { + "StartTime": 15375.0, + "Position": 401.624725 + }, { + "StartTime": 15437.0, + "Position": 388.561646 + }, { + "StartTime": 15500.0, + "Position": 423.803925 + }, { + "StartTime": 15562.0, + "Position": 425.740845 + }, { + "StartTime": 15625.0, + "Position": 449.983124 + }, { + "StartTime": 15687.0, + "Position": 468.920044 + }, { + "StartTime": 15750.0, + "Position": 492.162323 + }, { + "StartTime": 15812.0, + "Position": 506.784332 + }, { + "StartTime": 15875.0, + "Position": 474.226227 + }, { + "StartTime": 15937.0, + "Position": 482.978638 + }, { + "StartTime": 16000.0, + "Position": 446.420532 + }, { + "StartTime": 16058.0, + "Position": 418.4146 + }, { + "StartTime": 16116.0, + "Position": 425.408844 + }, { + "StartTime": 16174.0, + "Position": 383.402924 + }, { + "StartTime": 16232.0, + "Position": 363.397156 + }, { + "StartTime": 16290.0, + "Position": 343.391235 + }, { + "StartTime": 16348.0, + "Position": 328.385468 + }, { + "StartTime": 16406.0, + "Position": 322.3797 + }, { + "StartTime": 16500.0, + "Position": 291.1977 + }] + }, { + "StartTime": 17000.0, + "Objects": [{ + "StartTime": 17000.0, + "Position": 256.0 + }, { + "StartTime": 17062.0, + "Position": 228.16 + }, { + "StartTime": 17125.0, + "Position": 234.0 + }, { + "StartTime": 17187.0, + "Position": 202.16 + }, { + "StartTime": 17250.0, + "Position": 176.0 + }, { + "StartTime": 17312.0, + "Position": 210.84 + }, { + "StartTime": 17375.0, + "Position": 221.0 + }, { + "StartTime": 17437.0, + "Position": 219.84 + }, { + "StartTime": 17500.0, + "Position": 256.0 + }, { + "StartTime": 17562.0, + "Position": 219.16 + }, { + "StartTime": 17625.0, + "Position": 228.0 + }, { + "StartTime": 17687.0, + "Position": 203.16 + }, { + "StartTime": 17750.0, + "Position": 176.0 + }, { + "StartTime": 17803.0, + "Position": 174.959991 + }, { + "StartTime": 17857.0, + "Position": 214.23999 + }, { + "StartTime": 17910.0, + "Position": 228.200012 + }, { + "StartTime": 18000.0, + "Position": 256.0 + }] + }, { + "StartTime": 18500.0, + "Objects": [{ + "StartTime": 18500.0, + "Position": 362.0 + }, { + "StartTime": 18559.0, + "Position": 249.0 + }, { + "StartTime": 18618.0, + "Position": 357.0 + }, { + "StartTime": 18678.0, + "Position": 167.0 + }, { + "StartTime": 18737.0, + "Position": 477.0 + }, { + "StartTime": 18796.0, + "Position": 411.0 + }, { + "StartTime": 18856.0, + "Position": 254.0 + }, { + "StartTime": 18915.0, + "Position": 308.0 + }, { + "StartTime": 18975.0, + "Position": 399.0 + }, { + "StartTime": 19034.0, + "Position": 176.0 + }, { + "StartTime": 19093.0, + "Position": 14.0 + }, { + "StartTime": 19153.0, + "Position": 258.0 + }, { + "StartTime": 19212.0, + "Position": 221.0 + }, { + "StartTime": 19271.0, + "Position": 481.0 + }, { + "StartTime": 19331.0, + "Position": 92.0 + }, { + "StartTime": 19390.0, + "Position": 211.0 + }, { + "StartTime": 19450.0, + "Position": 135.0 + }] + }, { + "StartTime": 19875.0, + "Objects": [{ + "StartTime": 19875.0, + "Position": 216.0 + }, { + "StartTime": 19937.0, + "Position": 215.307053 + }, { + "StartTime": 20000.0, + "Position": 236.036865 + }, { + "StartTime": 20062.0, + "Position": 236.312088 + }, { + "StartTime": 20125.0, + "Position": 235.838928 + }, { + "StartTime": 20187.0, + "Position": 269.9743 + }, { + "StartTime": 20250.0, + "Position": 285.999146 + }, { + "StartTime": 20312.0, + "Position": 283.669067 + }, { + "StartTime": 20375.0, + "Position": 317.446747 + }, { + "StartTime": 20437.0, + "Position": 330.750275 + }, { + "StartTime": 20500.0, + "Position": 344.0156 + }, { + "StartTime": 20562.0, + "Position": 318.472168 + }, { + "StartTime": 20625.0, + "Position": 309.165466 + }, { + "StartTime": 20687.0, + "Position": 317.044617 + }, { + "StartTime": 20750.0, + "Position": 280.457367 + }, { + "StartTime": 20812.0, + "Position": 272.220581 + }, { + "StartTime": 20875.0, + "Position": 270.3294 + }, { + "StartTime": 20937.0, + "Position": 262.57605 + }, { + "StartTime": 21000.0, + "Position": 244.803329 + }, { + "StartTime": 21062.0, + "Position": 215.958359 + }, { + "StartTime": 21125.0, + "Position": 177.79332 + }, { + "StartTime": 21187.0, + "Position": 190.948349 + }, { + "StartTime": 21250.0, + "Position": 158.78334 + }, { + "StartTime": 21312.0, + "Position": 136.93837 + }, { + "StartTime": 21375.0, + "Position": 119.121056 + }, { + "StartTime": 21437.0, + "Position": 132.387573 + }, { + "StartTime": 21500.0, + "Position": 124.503014 + }, { + "StartTime": 21562.0, + "Position": 118.749374 + }, { + "StartTime": 21625.0, + "Position": 123.165535 + }, { + "StartTime": 21687.0, + "Position": 96.02999 + }, { + "StartTime": 21750.0, + "Position": 118.547928 + }, { + "StartTime": 21812.0, + "Position": 128.856232 + }, { + "StartTime": 21875.0, + "Position": 124.28746 + }, { + "StartTime": 21937.0, + "Position": 150.754929 + }, { + "StartTime": 22000.0, + "Position": 149.528732 + }, { + "StartTime": 22062.0, + "Position": 145.1691 + }, { + "StartTime": 22125.0, + "Position": 182.802155 + }, { + "StartTime": 22187.0, + "Position": 178.6452 + }, { + "StartTime": 22250.0, + "Position": 213.892181 + }, { + "StartTime": 22312.0, + "Position": 218.713028 + }, { + "StartTime": 22375.0, + "Position": 240.4715 + }, { + "StartTime": 22437.0, + "Position": 239.371887 + }, { + "StartTime": 22500.0, + "Position": 261.907257 + }, { + "StartTime": 22562.0, + "Position": 314.353119 + }, { + "StartTime": 22625.0, + "Position": 299.273376 + }, { + "StartTime": 22687.0, + "Position": 356.98288 + }, { + "StartTime": 22750.0, + "Position": 339.078552 + }, { + "StartTime": 22812.0, + "Position": 377.8958 + }, { + "StartTime": 22875.0, + "Position": 398.054047 + }, { + "StartTime": 22937.0, + "Position": 398.739441 + }, { + "StartTime": 23000.0, + "Position": 407.178467 + }, { + "StartTime": 23062.0, + "Position": 444.8687 + }, { + "StartTime": 23125.0, + "Position": 417.069977 + }, { + "StartTime": 23187.0, + "Position": 454.688477 + }, { + "StartTime": 23250.0, + "Position": 428.9612 + }, { + "StartTime": 23312.0, + "Position": 441.92807 + }, { + "StartTime": 23375.0, + "Position": 439.749878 + }, { + "StartTime": 23433.0, + "Position": 455.644684 + }, { + "StartTime": 23491.0, + "Position": 440.7359 + }, { + "StartTime": 23549.0, + "Position": 430.0944 + }, { + "StartTime": 23607.0, + "Position": 420.796173 + }, { + "StartTime": 23665.0, + "Position": 435.897461 + }, { + "StartTime": 23723.0, + "Position": 418.462555 + }, { + "StartTime": 23781.0, + "Position": 405.53775 + }, { + "StartTime": 23874.0, + "Position": 408.720825 + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic.osu new file mode 100644 index 0000000000..40b4409760 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Catch/Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch/Tests/CatchBeatmapConversionTest.cs new file mode 100644 index 0000000000..826c900140 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/CatchBeatmapConversionTest.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class CatchBeatmapConversionTest : BeatmapConversionTest + { + protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; + + [TestCase("basic"), Ignore("See: https://github.com/ppy/osu/issues/2149")] + public new void Test(string name) + { + base.Test(name); + } + + protected override IEnumerable CreateConvertValue(HitObject hitObject) + { + if (hitObject is JuiceStream stream) + { + foreach (var nested in stream.NestedHitObjects) + { + yield return new ConvertValue + { + StartTime = nested.StartTime, + Position = ((CatchHitObject)nested).X * CatchPlayfield.BASE_WIDTH + }; + } + } + else + { + yield return new ConvertValue + { + StartTime = hitObject.StartTime, + Position = ((CatchHitObject)hitObject).X * CatchPlayfield.BASE_WIDTH + }; + } + } + + protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new CatchBeatmapConverter(); + } + + public struct ConvertValue : IEquatable + { + /// + /// A sane value to account for osu!stable using ints everwhere. + /// + private const float conversion_lenience = 2; + + public double StartTime; + public float Position; + + public bool Equals(ConvertValue other) + => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) + && Precision.AlmostEquals(Position, other.Position, conversion_lenience); + } +} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseFruitObjects.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseFruitObjects.cs index 1b348f9c8c..16266196e7 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseFruitObjects.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseFruitObjects.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.MathUtils; @@ -15,6 +16,7 @@ using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Tests { + [TestFixture] public class TestCaseFruitObjects : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs index e2760795c9..2be6dd005d 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs @@ -1,8 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; + namespace osu.Game.Rulesets.Catch.Tests { + [TestFixture] public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints { public TestCasePerformancePoints() diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 956a524121..41dd7fdf4e 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Input; using osu.Game.Beatmaps; +using osu.Game.Input.Handlers; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this); - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj index 894fdc9b45..4e2cdd24c3 100644 --- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj +++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj @@ -95,6 +95,7 @@ + @@ -128,6 +129,10 @@ + + + + diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index aeefc2f396..cb500735f7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs @@ -14,5 +14,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The number of s which this stage contains. /// public int Columns; + + /// + /// Whether the column index is a special column for this stage. + /// + /// The 0-based column index. + /// Whether the column is a special column. + public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2; } } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index ac815e0e2f..e135e14001 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -12,6 +12,8 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Mania { @@ -89,6 +91,7 @@ namespace osu.Game.Rulesets.Mania }, new ManiaModRandom(), new ManiaModDualStages(), + new ManiaModMirror(), new MultiMod { Mods = new Mod[] @@ -112,7 +115,9 @@ namespace osu.Game.Rulesets.Mania public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods); - public override int LegacyID => 3; + public override int? LegacyID => 3; + + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); public ManiaRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) diff --git a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs index e14473c478..c8277af415 100644 --- a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs +++ b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils /// internal class FastRandom { - private const double uint_to_real = 1.0 / (uint.MaxValue + 1.0); + private const double int_to_real = 1.0 / (int.MaxValue + 1.0); private const uint int_mask = 0x7FFFFFFF; private const uint y = 842502087; private const uint z = 3579807591; @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils /// Generates a random double value within the range [0, 1). /// /// The random value. - public double NextDouble() => uint_to_real * NextUInt(); + public double NextDouble() => int_to_real * Next(); private uint bitBuffer; private int bitIndex = 32; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs index 3c5179cef0..9ceb0ab7ea 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mods; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods return new Score { User = new User { Username = "osu!topus!" }, - Replay = new ManiaAutoGenerator(beatmap).Generate(), + Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), }; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs new file mode 100644 index 0000000000..cfa5ef88b8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using System.Linq; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModMirror : Mod, IApplicableToRulesetContainer + { + public override string Name => "Mirror"; + public override string ShortenedName => "MR"; + public override ModType Type => ModType.Special; + public override double ScoreMultiplier => 1; + public override bool Ranked => true; + + public void ApplyToRulesetContainer(RulesetContainer rulesetContainer) + { + var availableColumns = ((ManiaRulesetContainer)rulesetContainer).Beatmap.TotalColumns; + + rulesetContainer.Objects.OfType().ForEach(h => h.Column = availableColumns - 1 - h.Column); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 6f6217f540..5a992bb970 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Replays; @@ -15,10 +15,31 @@ namespace osu.Game.Rulesets.Mania.Replays { public const double RELEASE_DELAY = 20; - public ManiaAutoGenerator(Beatmap beatmap) + public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap; + + private readonly ManiaAction[] columnActions; + + public ManiaAutoGenerator(ManiaBeatmap beatmap) : base(beatmap) { Replay = new Replay { User = new User { Username = @"Autoplay" } }; + + columnActions = new ManiaAction[Beatmap.TotalColumns]; + + var normalAction = ManiaAction.Key1; + var specialAction = ManiaAction.Special1; + int totalCounter = 0; + foreach (var stage in Beatmap.Stages) + { + for (int i = 0; i < stage.Columns; i++) + { + if (stage.IsSpecialColumn(i)) + columnActions[totalCounter] = specialAction++; + else + columnActions[totalCounter] = normalAction++; + totalCounter++; + } + } } protected Replay Replay; @@ -30,18 +51,18 @@ namespace osu.Game.Rulesets.Mania.Replays var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); - int activeColumns = 0; + var actions = new List(); foreach (var group in pointGroups) { foreach (var point in group) { if (point is HitPoint) - activeColumns |= 1 << point.Column; + actions.Add(columnActions[point.Column]); if (point is ReleasePoint) - activeColumns ^= 1 << point.Column; + actions.Remove(columnActions[point.Column]); } - Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, activeColumns)); + Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray())); } return Replay; diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs index fd084f138f..3541561418 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs @@ -4,40 +4,19 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Input; -using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Mania.Replays { - internal class ManiaFramedReplayInputHandler : FramedReplayInputHandler + internal class ManiaFramedReplayInputHandler : FramedReplayInputHandler { - private readonly ManiaRulesetContainer container; - - public ManiaFramedReplayInputHandler(Replay replay, ManiaRulesetContainer container) + public ManiaFramedReplayInputHandler(Replay replay) : base(replay) { - this.container = container; } - private ManiaPlayfield playfield; - public override List GetPendingStates() - { - var actions = new List(); + protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any(); - if (playfield == null) - playfield = (ManiaPlayfield)container.Playfield; - - int activeColumns = (int)(CurrentFrame.MouseX ?? 0); - int counter = 0; - while (activeColumns > 0) - { - if ((activeColumns & 1) > 0) - actions.Add(playfield.Columns.ElementAt(counter).Action); - counter++; - activeColumns >>= 1; - } - - return new List { new ReplayState { PressedActions = actions } }; - } + public override List GetPendingStates() => new List { new ReplayState { PressedActions = CurrentFrame.Actions } }; } } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index e5c5ac9eeb..9990f89b99 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -1,17 +1,59 @@ // Copyright (c) 2007-2018 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.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Mania.Replays { - public class ManiaReplayFrame : ReplayFrame + public class ManiaReplayFrame : ReplayFrame, IConvertibleReplayFrame { - public override bool IsImportant => MouseX > 0; + public List Actions = new List(); - public ManiaReplayFrame(double time, int activeColumns) - : base(time, activeColumns, null, ReplayButtonState.None) + public ManiaReplayFrame() { } + + public ManiaReplayFrame(double time, params ManiaAction[] actions) + : base(time) + { + Actions.AddRange(actions); + } + + public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + { + // We don't need to fully convert, just create the converter + var converter = new ManiaBeatmapConverter(beatmap.BeatmapInfo.RulesetID == 3, beatmap); + + // NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling + // elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage. + + var stage = new StageDefinition { Columns = converter.TargetColumns }; + + var normalAction = ManiaAction.Key1; + var specialAction = ManiaAction.Special1; + + int activeColumns = (int)(legacyFrame.MouseX ?? 0); + int counter = 0; + while (activeColumns > 0) + { + var isSpecial = stage.IsSpecialColumn(counter); + + if ((activeColumns & 1) > 0) + Actions.Add(isSpecial ? specialAction : normalAction); + + if (isSpecial) + specialAction++; + else + normalAction++; + + counter++; + activeColumns >>= 1; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json new file mode 100644 index 0000000000..d593b2b052 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -0,0 +1,103 @@ +{ + "Mappings": [{ + "StartTime": 500, + "Objects": [{ + "StartTime": 500, + "EndTime": 2500, + "Column": 0 + }, + { + "StartTime": 1500, + "EndTime": 2500, + "Column": 1 + } + ] + }, + { + "StartTime": 3000, + "Objects": [{ + "StartTime": 3000, + "EndTime": 4000, + "Column": 2 + }] + }, + { + "StartTime": 4500, + "Objects": [{ + "StartTime": 4500, + "EndTime": 5500, + "Column": 4 + }] + }, + { + "StartTime": 6000, + "Objects": [{ + "StartTime": 6000, + "EndTime": 6500, + "Column": 2 + }] + }, + { + "StartTime": 7000, + "Objects": [{ + "StartTime": 7000, + "EndTime": 8000, + "Column": 2 + }] + }, + { + "StartTime": 8500, + "Objects": [{ + "StartTime": 8500, + "EndTime": 11000, + "Column": 0 + }] + }, + { + "StartTime": 11500, + "Objects": [{ + "StartTime": 11500, + "EndTime": 12000, + "Column": 1 + }] + }, + { + "StartTime": 12500, + "Objects": [{ + "StartTime": 12500, + "EndTime": 16500, + "Column": 4 + }] + }, + { + "StartTime": 17000, + "Objects": [{ + "StartTime": 17000, + "EndTime": 18000, + "Column": 2 + }] + }, + { + "StartTime": 18500, + "Objects": [{ + "StartTime": 18500, + "EndTime": 19450, + "Column": 0 + }] + }, + { + "StartTime": 19875, + "Objects": [{ + "StartTime": 19875, + "EndTime": 23875, + "Column": 1 + }, + { + "StartTime": 19875, + "EndTime": 23875, + "Column": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic.osu new file mode 100644 index 0000000000..40b4409760 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Mania/Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania/Tests/ManiaBeatmapConversionTest.cs new file mode 100644 index 0000000000..2095addc72 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Tests/ManiaBeatmapConversionTest.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class ManiaBeatmapConversionTest : BeatmapConversionTest + { + protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; + + private bool isForCurrentRuleset; + + [NonParallelizable] + [TestCase("basic", false), Ignore("See: https://github.com/ppy/osu/issues/2150")] + public void Test(string name, bool isForCurrentRuleset) + { + this.isForCurrentRuleset = isForCurrentRuleset; + base.Test(name); + } + + protected override IEnumerable CreateConvertValue(HitObject hitObject) + { + yield return new ConvertValue + { + StartTime = hitObject.StartTime, + EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime, + Column = ((ManiaHitObject)hitObject).Column + }; + } + + protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new ManiaBeatmapConverter(isForCurrentRuleset, beatmap); + } + + public struct ConvertValue : IEquatable + { + /// + /// A sane value to account for osu!stable using ints everwhere. + /// + private const float conversion_lenience = 2; + + public double StartTime; + public double EndTime; + public int Column; + + public bool Equals(ConvertValue other) + => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) + && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) + && Column == other.Column; + } +} diff --git a/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs b/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs index 07fb6ac670..2453d8281a 100644 --- a/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs +++ b/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs @@ -1,14 +1,17 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using NUnit.Framework; -using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests { + [TestFixture] public class TestCaseAutoGeneration : OsuTestCase { [Test] @@ -18,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | - | // | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); beatmap.HitObjects.Add(new Note { StartTime = 1000 }); var generated = new ManiaAutoGenerator(beatmap).Generate(); @@ -26,8 +29,8 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); - Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 0 has not been pressed"); - Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 0 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released"); } [Test] @@ -39,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | // | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); var generated = new ManiaAutoGenerator(beatmap).Generate(); @@ -47,8 +50,8 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); - Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 0 has not been pressed"); - Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 0 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released"); } [Test] @@ -58,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | - | - | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new Note { StartTime = 1000 }); beatmap.HitObjects.Add(new Note { StartTime = 1000, Column = 1 }); @@ -67,8 +70,8 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); - Assert.AreEqual(3, generated.Frames[1].MouseX, "Keys 1 and 2 have not been pressed"); - Assert.AreEqual(0, generated.Frames[2].MouseX, "Keys 1 and 2 have not been released"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); } [Test] @@ -80,7 +83,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | * | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000, Column = 1 }); @@ -89,8 +92,8 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); - Assert.AreEqual(3, generated.Frames[1].MouseX, "Keys 1 and 2 have not been pressed"); - Assert.AreEqual(0, generated.Frames[2].MouseX, "Keys 1 and 2 have not been released"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); } [Test] @@ -101,7 +104,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | - | | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new Note { StartTime = 1000 }); beatmap.HitObjects.Add(new Note { StartTime = 2000, Column = 1 }); @@ -112,10 +115,10 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect first note release time"); Assert.AreEqual(2000, generated.Frames[3].Time, "Incorrect second note hit time"); Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); - Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed"); - Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 1 has not been released"); - Assert.AreEqual(2, generated.Frames[3].MouseX, "Key 2 has not been pressed"); - Assert.AreEqual(0, generated.Frames[4].MouseX, "Key 2 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released"); } [Test] @@ -128,7 +131,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 2000, Duration = 2000, Column = 1 }); @@ -139,10 +142,11 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect first note release time"); Assert.AreEqual(2000, generated.Frames[2].Time, "Incorrect second note hit time"); Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); - Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed"); - Assert.AreEqual(3, generated.Frames[2].MouseX, "Keys 1 and 2 have not been pressed"); - Assert.AreEqual(2, generated.Frames[3].MouseX, "Key 1 has not been released"); - Assert.AreEqual(0, generated.Frames[4].MouseX, "Key 2 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key1), "Key1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has been released"); + Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released"); } [Test] @@ -154,7 +158,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 - ManiaAutoGenerator.RELEASE_DELAY }); beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 }); @@ -164,9 +168,12 @@ namespace osu.Game.Rulesets.Mania.Tests Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); Assert.AreEqual(3000, generated.Frames[2].Time, "Incorrect second note press time + first note release time"); Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect second note release time"); - Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed"); - Assert.AreEqual(2, generated.Frames[2].MouseX, "Key 1 has not been released or key 2 has not been pressed"); - Assert.AreEqual(0, generated.Frames[3].MouseX, "Keys 1 and 2 have not been released"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key2), "Key2 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been released"); } + + private bool checkContains(ReplayFrame frame, params ManiaAction[] actions) => actions.All(action => ((ManiaReplayFrame)frame).Actions.Contains(action)); } } diff --git a/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs index e3aa4c1fd6..3c776a2f4c 100644 --- a/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs +++ b/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs @@ -1,8 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; + namespace osu.Game.Rulesets.Mania.Tests { + [TestFixture] public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints { public TestCasePerformancePoints() diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 8a03f5a785..b8ae09c4a0 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -3,13 +3,14 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI { internal class DrawableManiaJudgement : DrawableJudgement { - public DrawableManiaJudgement(Judgement judgement) - : base(judgement) + public DrawableManiaJudgement(Judgement judgement, DrawableHitObject judgedObject) + : base(judgement, judgedObject) { JudgementText.TextSize = 25; } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 732d5f4109..3ecfee1e8c 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -11,6 +11,7 @@ using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Input.Handlers; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Mods; @@ -103,7 +104,7 @@ namespace osu.Game.Rulesets.Mania.UI protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f); - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay, this); + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); protected override IRulesetConfigManager CreateConfig(Ruleset ruleset, SettingsStore settings) => new ManiaConfigManager(settings, Ruleset.RulesetInfo, Variant); } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index ebd73d7dca..d4ca704829 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using OpenTK; using OpenTK.Graphics; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Container content; public Container Judgements => judgements; - private readonly Container judgements; + private readonly JudgementContainer judgements; private readonly Container topLevelContainer; @@ -48,13 +49,11 @@ namespace osu.Game.Rulesets.Mania.UI private Color4 specialColumnColour; private readonly int firstColumnIndex; - private readonly StageDefinition definition; public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) : base(ScrollingDirection.Up) { this.firstColumnIndex = firstColumnIndex; - this.definition = definition; Name = "Stage"; @@ -116,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.UI Padding = new MarginPadding { Top = HIT_TARGET_POSITION } } }, - judgements = new Container + judgements = new JudgementContainer { Anchor = Anchor.TopCentre, Origin = Anchor.Centre, @@ -131,7 +130,7 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < definition.Columns; i++) { - var isSpecial = isSpecialColumn(i); + var isSpecial = definition.IsSpecialColumn(i); var column = new Column { IsSpecial = isSpecial, @@ -160,13 +159,6 @@ namespace osu.Game.Rulesets.Mania.UI AddNested(c); } - /// - /// Whether the column index is a special column for this playfield. - /// - /// The 0-based column index. - /// Whether the column is a special column. - private bool isSpecialColumn(int column) => definition.Columns % 2 == 1 && column == definition.Columns / 2; - public override void Add(DrawableHitObject h) { var maniaObject = (ManiaHitObject)h.HitObject; @@ -180,7 +172,7 @@ namespace osu.Game.Rulesets.Mania.UI internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) { judgements.Clear(); - judgements.Add(new DrawableManiaJudgement(judgement) + judgements.Add(new DrawableManiaJudgement(judgement, judgedObject) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index b9c62cf40b..39b856b67b 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -81,6 +81,7 @@ + @@ -127,6 +128,7 @@ + @@ -160,6 +162,10 @@ + + + + diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/OsuHitObjectOverlayLayer.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/OsuHitObjectOverlayLayer.cs new file mode 100644 index 0000000000..e0d1b34ca5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/OsuHitObjectOverlayLayer.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Edit.Layers.Selection; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection +{ + public class OsuHitObjectOverlayLayer : HitObjectOverlayLayer + { + protected override HitObjectOverlay CreateOverlayFor(DrawableHitObject hitObject) + { + switch (hitObject) + { + case DrawableHitCircle circle: + return new HitCircleOverlay(circle); + case DrawableSlider slider: + return new SliderOverlay(slider); + } + + return base.CreateOverlayFor(hitObject); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleOverlay.cs new file mode 100644 index 0000000000..4e64783840 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleOverlay.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit.Layers.Selection; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; + +namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays +{ + public class HitCircleOverlay : HitObjectOverlay + { + public HitCircleOverlay(DrawableHitCircle hitCircle) + : base(hitCircle) + { + Origin = Anchor.Centre; + + Position = hitCircle.Position; + Size = hitCircle.Size; + Scale = hitCircle.Scale; + + AddInternal(new RingPiece()); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Yellow; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleOverlay.cs new file mode 100644 index 0000000000..3c7f8a067b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleOverlay.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit.Layers.Selection; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays +{ + public class SliderCircleOverlay : HitObjectOverlay + { + public SliderCircleOverlay(DrawableHitCircle sliderHead, DrawableSlider slider) + : this(sliderHead, sliderHead.Position, slider) + { + } + + public SliderCircleOverlay(DrawableSliderTail sliderTail, DrawableSlider slider) + : this(sliderTail, sliderTail.Position, slider) + { + } + + private readonly DrawableOsuHitObject hitObject; + + private SliderCircleOverlay(DrawableOsuHitObject hitObject, Vector2 position, DrawableSlider slider) + : base(hitObject) + { + this.hitObject = hitObject; + + Origin = Anchor.Centre; + + Position = position; + Size = slider.HeadCircle.Size; + Scale = slider.HeadCircle.Scale; + + AddInternal(new RingPiece()); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Yellow; + } + + protected override void Update() + { + base.Update(); + + RelativeAnchorPosition = hitObject.RelativeAnchorPosition; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderOverlay.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderOverlay.cs new file mode 100644 index 0000000000..a035a683e9 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderOverlay.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit.Layers.Selection; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays +{ + public class SliderOverlay : HitObjectOverlay + { + private readonly SliderBody body; + private readonly DrawableSlider slider; + + public SliderOverlay(DrawableSlider slider) + : base(slider) + { + this.slider = slider; + + var obj = (Slider)slider.HitObject; + + InternalChildren = new Drawable[] + { + body = new SliderBody(obj) + { + AccentColour = Color4.Transparent, + PathWidth = obj.Scale * 64 + }, + new SliderCircleOverlay(slider.HeadCircle, slider), + new SliderCircleOverlay(slider.TailCircle, slider), + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + body.BorderColour = colours.Yellow; + } + + protected override void Update() + { + base.Update(); + + Position = slider.Position; + Size = slider.Size; + OriginPosition = slider.OriginPosition; + + // Need to cause one update + body.UpdateProgress(0); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index ae19706da3..70d49a6b4f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -5,7 +5,9 @@ using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Layers.Selection; using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Osu.Edit.Layers.Selection; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; @@ -29,5 +31,7 @@ namespace osu.Game.Rulesets.Osu.Edit }; protected override ScalableContainer CreateLayerContainer() => new ScalableContainer(OsuPlayfield.BASE_SIZE.X) { RelativeSizeAxes = Axes.Both }; + + protected override HitObjectOverlayLayer CreateHitObjectOverlayLayer() => new OsuHitObjectOverlayLayer(); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index dfbe9ad021..29bf3e248d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; @@ -22,8 +24,14 @@ namespace osu.Game.Rulesets.Osu.Mods if (slider == null) return; + slider.HeadCircle.Position = new Vector2(slider.HeadCircle.Position.X, OsuPlayfield.BASE_SIZE.Y - slider.HeadCircle.Position.Y); + slider.TailCircle.Position = new Vector2(slider.TailCircle.Position.X, OsuPlayfield.BASE_SIZE.Y - slider.TailCircle.Position.Y); + + slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); + slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); + var newControlPoints = new List(); - slider.ControlPoints.ForEach(c => newControlPoints.Add(new Vector2(c.X, OsuPlayfield.BASE_SIZE.Y - c.Y))); + slider.ControlPoints.ForEach(c => newControlPoints.Add(new Vector2(c.X, -c.Y))); slider.ControlPoints = newControlPoints; slider.Curve?.Calculate(); // Recalculate the slider curve diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index beabeb0a19..4aeb76121a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods @@ -25,7 +26,10 @@ namespace osu.Game.Rulesets.Osu.Mods foreach (var d in drawables.OfType()) { d.ApplyCustomUpdateState += ApplyHiddenState; + d.HitObject.TimeFadein = d.HitObject.TimePreempt * fade_in_duration_multiplier; + foreach (var h in d.HitObject.NestedHitObjects.OfType()) + h.TimeFadein = h.TimePreempt * fade_in_duration_multiplier; } } @@ -34,17 +38,20 @@ namespace osu.Game.Rulesets.Osu.Mods if (!(drawable is DrawableOsuHitObject d)) return; - var fadeOutStartTime = d.HitObject.StartTime - d.HitObject.TimePreempt + d.HitObject.TimeFadein; - var fadeOutDuration = d.HitObject.TimePreempt * fade_out_duration_multiplier; + var h = d.HitObject; + + var fadeOutStartTime = h.StartTime - h.TimePreempt + h.TimeFadein; + var fadeOutDuration = h.TimePreempt * fade_out_duration_multiplier; // new duration from completed fade in to end (before fading out) - var longFadeDuration = ((d.HitObject as IHasEndTime)?.EndTime ?? d.HitObject.StartTime) - fadeOutStartTime; + var longFadeDuration = ((h as IHasEndTime)?.EndTime ?? h.StartTime) - fadeOutStartTime; switch (drawable) { case DrawableHitCircle circle: // we don't want to see the approach circle - circle.ApproachCircle.Hide(); + using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + circle.ApproachCircle.Hide(); // fade out immediately after fade in. using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 716f4b629b..0b1df4bdf5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -2,17 +2,17 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Game.Rulesets.Osu.Judgements; using OpenTK; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuJudgement : DrawableJudgement { - public DrawableOsuJudgement(OsuJudgement judgement) - : base(judgement) + public DrawableOsuJudgement(Judgement judgement, DrawableHitObject judgedObject) + : base(judgement, judgedObject) { } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index db704b0553..79a4714e33 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -78,8 +78,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables bool isRepeatAtEnd = repeatPoint.RepeatIndex % 2 == 0; List curve = drawableSlider.Body.CurrentCurve; - var positionOnCurve = isRepeatAtEnd ? end : start; - Position = positionOnCurve + drawableSlider.HitObject.StackOffset; + Position = isRepeatAtEnd ? end : start; if (curve.Count < 2) return; @@ -90,10 +89,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // find the next vector2 in the curve which is not equal to our current position to infer a rotation. for (int i = searchStart; i >= 0 && i < curve.Count; i += direction) { - if (curve[i] == positionOnCurve) + if (curve[i] == Position) continue; - Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(curve[i].Y - positionOnCurve.Y, curve[i].X - positionOnCurve.X)); + Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(curve[i].Y - Position.Y, curve[i].X - Position.X)); break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 391e0ff023..5b9ed4d259 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly List components = new List(); public readonly DrawableHitCircle HeadCircle; + public readonly DrawableSliderTail TailCircle; + public readonly SliderBody Body; public readonly SliderBall Ball; @@ -30,7 +32,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { slider = s; - DrawableSliderTail tail; + Position = s.StackedPosition; + Container ticks; Container repeatPoints; @@ -39,20 +42,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Body = new SliderBody(s) { AccentColour = AccentColour, - Position = s.StackedPosition, PathWidth = s.Scale * 64, }, - ticks = new Container(), - repeatPoints = new Container(), + ticks = new Container { RelativeSizeAxes = Axes.Both }, + repeatPoints = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s) { + BypassAutoSizeAxes = Axes.Both, Scale = new Vector2(s.Scale), AccentColour = AccentColour, AlwaysPresent = true, Alpha = 0 }, - HeadCircle = new DrawableHitCircle(s.HeadCircle), - tail = new DrawableSliderTail(s.TailCircle) + HeadCircle = new DrawableSliderHead(s, s.HeadCircle), + TailCircle = new DrawableSliderTail(s, s.TailCircle) }; components.Add(Body); @@ -60,15 +63,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AddNested(HeadCircle); - AddNested(tail); - components.Add(tail); + AddNested(TailCircle); + components.Add(TailCircle); foreach (var tick in s.NestedHitObjects.OfType()) { - var drawableTick = new DrawableSliderTick(tick) - { - Position = tick.StackedPosition - }; + var drawableTick = new DrawableSliderTick(tick) { Position = tick.Position - s.Position }; ticks.Add(drawableTick); components.Add(drawableTick); @@ -77,10 +77,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var repeatPoint in s.NestedHitObjects.OfType()) { - var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) - { - Position = repeatPoint.StackedPosition - }; + var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) { Position = repeatPoint.Position - s.Position }; repeatPoints.Add(drawableRepeatPoint); components.Add(drawableRepeatPoint); @@ -105,13 +102,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); - //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. - if (!HeadCircle.IsHit) - HeadCircle.Position = slider.StackedPositionAt(completionProgress); - foreach (var c in components.OfType()) c.UpdateProgress(completionProgress); foreach (var c in components.OfType()) c.UpdateSnakingPosition(slider.Curve.PositionAt(Body.SnakedStart ?? 0), slider.Curve.PositionAt(Body.SnakedEnd ?? 0)); foreach (var t in components.OfType()) t.Tracking = Ball.Tracking; + + Size = Body.Size; + OriginPosition = Body.PathOffset; + + if (DrawSize != Vector2.Zero) + { + var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize); + foreach (var obj in NestedHitObjects) + obj.RelativeAnchorPosition = childAnchorPosition; + Ball.RelativeAnchorPosition = childAnchorPosition; + } } protected override void CheckForJudgements(bool userTriggered, double timeOffset) @@ -154,13 +158,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); } + + Expire(true); } public Drawable ProxiedLayer => HeadCircle.ApproachCircle; public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Body.ReceiveMouseInputAt(screenSpacePos); - public override Vector2 SelectionPoint => ToScreenSpace(Body.Position); + public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition); public override Quad SelectionQuad => Body.PathDrawQuad; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs new file mode 100644 index 0000000000..cf36d5fc14 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Types; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class DrawableSliderHead : DrawableHitCircle + { + private readonly Slider slider; + + public DrawableSliderHead(Slider slider, HitCircle h) + : base(h) + { + this.slider = slider; + + Position = HitObject.Position - slider.Position; + } + + protected override void Update() + { + base.Update(); + + double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); + + //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. + if (!IsHit) + Position = slider.CurvePositionAt(completionProgress); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 8835fc2b29..b277e7df7a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -16,11 +16,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } - public DrawableSliderTail(HitCircle hitCircle) + public DrawableSliderTail(Slider slider, HitCircle hitCircle) : base(hitCircle) { - AlwaysPresent = true; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + AlwaysPresent = true; + + Position = HitObject.Position - slider.Position; } protected override void CheckForJudgements(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index baa9eac1a3..058e3606f4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -50,10 +50,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdatePreemptState() { - this.Animate( - d => d.FadeIn(ANIM_DURATION), - d => d.ScaleTo(0.5f).ScaleTo(1f, ANIM_DURATION * 4, Easing.OutElasticHalf) - ); + this.FadeOut().FadeIn(ANIM_DURATION); + this.ScaleTo(0.5f).ScaleTo(1f, ANIM_DURATION * 4, Easing.OutElasticHalf); } protected override void UpdateCurrentState(ArmedState state) @@ -64,12 +62,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.Delay(HitObject.TimePreempt).FadeOut(); break; case ArmedState.Miss: - this.FadeOut(ANIM_DURATION) - .FadeColour(Color4.Red, ANIM_DURATION / 2); + this.FadeOut(ANIM_DURATION); + this.FadeColour(Color4.Red, ANIM_DURATION / 2); break; case ArmedState.Hit: - this.FadeOut(ANIM_DURATION, Easing.OutQuint) - .ScaleTo(Scale * 1.5f, ANIM_DURATION, Easing.Out); + this.FadeOut(ANIM_DURATION, Easing.OutQuint); + this.ScaleTo(Scale * 1.5f, ANIM_DURATION, Easing.Out); break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs index 61e9027157..51f8b7026a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs @@ -6,30 +6,24 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class ApproachCircle : Container { - private readonly Sprite approachCircle; - public ApproachCircle() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - approachCircle = new Sprite() - }; + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(TextureStore textures) { - approachCircle.Texture = textures.Get(@"Play/osu/approachcircle"); + Child = new SkinnableDrawable("Play/osu/approachcircle", name => new Sprite { Texture = textures.Get(name) }); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index 286df14056..e7b6598cf2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -2,20 +2,16 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; +using osu.Game.Skinning; using OpenTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class CirclePiece : Container, IKeyBindingHandler { - private readonly Sprite disc; - public Func Hit; public CirclePiece() @@ -27,26 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Anchor = Anchor.Centre; Origin = Anchor.Centre; - Children = new Drawable[] - { - disc = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - new TrianglesPiece - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Alpha = 0.5f, - } - }; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - disc.Texture = textures.Get(@"Play/osu/disc"); + InternalChild = new SkinnableDrawable("Play/osu/hitcircle", _ => new DefaultCirclePiece()); } public bool OnPressed(OsuAction action) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs new file mode 100644 index 0000000000..61f73b6d66 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + public class DefaultCirclePiece : Container + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + RelativeSizeAxes = Axes.Both; + Children = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get(@"Play/osu/disc"), + }, + new TrianglesPiece + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingMode.Additive, + Alpha = 0.5f, + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs index 9a1208f998..a4e1916659 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs @@ -6,34 +6,30 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class GlowPiece : Container { - private readonly Sprite layer; - public GlowPiece() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - - Children = new[] - { - layer = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Blending = BlendingMode.Additive, - Alpha = 0.5f - } - }; + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(TextureStore textures) { - layer.Texture = textures.Get(@"Play/osu/ring-glow"); + Child = new SkinnableDrawable("Play/osu/ring-glow", name => new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get(name), + Blending = BlendingMode.Additive, + Alpha = 0.5f + }, false); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs index afbf00f320..4220299c66 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Children = new Drawable[] { - new CircularContainer + new SkinnableDrawable("Play/osu/number-glow", name => new CircularContainer { Masking = true, Origin = Anchor.Centre, @@ -38,11 +39,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Radius = 60, Colour = Color4.White.Opacity(0.5f), }, - Children = new[] - { - new Box() - } - }, + Child = new Box() + }, false), number = new OsuSpriteText { Text = @"1", diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs index 2347927f2e..12cc0dc5d9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -15,24 +16,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Size = new Vector2(128); - Masking = true; - CornerRadius = Size.X / 2; - Anchor = Anchor.Centre; Origin = Anchor.Centre; - BorderThickness = 10; - BorderColour = Color4.White; - - Children = new Drawable[] + InternalChild = new SkinnableDrawable("Play/osu/hitcircleoverlay", _ => new Container { - new Box + Masking = true, + CornerRadius = Size.X / 2, + BorderThickness = 10, + BorderColour = Color4.White, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - AlwaysPresent = true, - Alpha = 0, - RelativeSizeAxes = Axes.Both + new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + } } - }; + }); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 61db10b694..1921c51889 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using osu.Game.Rulesets.Objects.Types; using OpenTK; using OpenTK.Graphics; @@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public void UpdateProgress(double completionProgress) { - Position = slider.StackedPositionAt(completionProgress); + Position = slider.CurvePositionAt(completionProgress); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index a83ee3a2e1..8c0eb7ff7d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -29,6 +29,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces set { path.PathWidth = value; } } + /// + /// Offset in absolute coordinates from the start of the curve. + /// + public Vector2 PathOffset { get; private set; } + + public readonly List CurrentCurve = new List(); + public readonly Bindable SnakingIn = new Bindable(); public readonly Bindable SnakingOut = new Bindable(); @@ -75,6 +82,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private int textureWidth => (int)PathWidth * 2; + private Vector2 topLeftOffset; + private readonly Slider slider; public SliderBody(Slider s) { @@ -84,6 +93,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { container = new BufferedContainer { + RelativeSizeAxes = Axes.Both, CacheDrawnFrameBuffer = true, Children = new Drawable[] { @@ -107,11 +117,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (updateSnaking(p0, p1)) { - // Autosizing does not give us the desired behaviour here. - // We want the container to have the same size as the slider, - // and to be positioned such that the slider head is at (0,0). - container.Size = path.Size; - container.Position = -path.PositionInBoundingBox(slider.Curve.PositionAt(0) - CurrentCurve[0]); + // The path is generated such that its size encloses it. This change of size causes the path + // to move around while snaking, so we need to offset it to make sure it maintains the + // same position as when it is fully snaked. + var newTopLeftOffset = path.PositionInBoundingBox(Vector2.Zero); + path.Position = topLeftOffset - newTopLeftOffset; container.ForceRedraw(); } @@ -121,6 +131,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private void load() { reloadTexture(); + computeSize(); } private void reloadTexture() @@ -164,7 +175,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces path.Texture = texture; } - public readonly List CurrentCurve = new List(); + private void computeSize() + { + // Generate the entire curve + slider.Curve.GetPathToProgress(CurrentCurve, 0, 1); + foreach (Vector2 p in CurrentCurve) + path.AddVertex(p); + + Size = path.Size; + + topLeftOffset = path.PositionInBoundingBox(Vector2.Zero); + PathOffset = path.PositionInBoundingBox(CurrentCurve[0]); + } + private bool updateSnaking(double p0, double p1) { if (SnakedStart == p0 && SnakedEnd == p1) return false; @@ -176,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces path.ClearVertices(); foreach (Vector2 p in CurrentCurve) - path.AddVertex(p - CurrentCurve[0]); + path.AddVertex(p); return true; } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index ce6c88a340..76439ca530 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -3,7 +3,6 @@ using OpenTK; using osu.Game.Rulesets.Objects.Types; -using System; using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; @@ -23,8 +22,8 @@ namespace osu.Game.Rulesets.Osu.Objects public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; public double Duration => EndTime - StartTime; - public Vector2 StackedPositionAt(double t) => this.PositionAt(t) + StackOffset; - public override Vector2 EndPosition => this.PositionAt(1); + public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t); + public override Vector2 EndPosition => Position + this.CurvePositionAt(1); public SliderCurve Curve { get; } = new SliderCurve(); @@ -99,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects HeadCircle = new HitCircle { StartTime = StartTime, - Position = StackedPosition, + Position = Position, IndexInCurrentCombo = IndexInCurrentCombo, ComboColour = ComboColour, Samples = Samples, @@ -109,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Objects TailCircle = new HitCircle { StartTime = EndTime, - Position = StackedEndPosition, + Position = EndPosition, IndexInCurrentCombo = IndexInCurrentCombo, ComboColour = ComboColour }; @@ -120,14 +119,16 @@ namespace osu.Game.Rulesets.Osu.Objects private void createTicks() { - if (TickDistance == 0) return; - var length = Curve.Distance; - var tickDistance = Math.Min(TickDistance, length); + var tickDistance = MathHelper.Clamp(TickDistance, 0, length); + + if (tickDistance == 0) return; var minDistanceFromEnd = Velocity * 0.01; - for (var span = 0; span < this.SpanCount(); span++) + var spanCount = this.SpanCount(); + + for (var span = 0; span < spanCount; span++) { var spanStartTime = StartTime + span * SpanDuration; var reversed = span % 2 == 1; @@ -156,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Objects SpanIndex = span, SpanStartTime = spanStartTime, StartTime = spanStartTime + timeProgress * SpanDuration, - Position = Curve.PositionAt(distanceProgress), + Position = Position + Curve.PositionAt(distanceProgress), StackHeight = StackHeight, Scale = Scale, ComboColour = ComboColour, @@ -175,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Objects RepeatIndex = repeatIndex, SpanDuration = SpanDuration, StartTime = StartTime + repeat * SpanDuration, - Position = Curve.PositionAt(repeat % 2), + Position = Position + Curve.PositionAt(repeat % 2), StackHeight = StackHeight, Scale = Scale, ComboColour = ComboColour, diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 2f238bb74b..b30e4cb932 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5)); // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. - SpinsRequired = (int)(SpinsRequired * 0.6); + SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index b38f95694f..d407835a96 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -19,6 +19,8 @@ using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Osu { @@ -143,7 +145,9 @@ namespace osu.Game.Rulesets.Osu public override SettingsSubsection CreateSettings() => new OsuSettings(); - public override int LegacyID => 0; + public override int? LegacyID => 0; + + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public OsuRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 274f7bff62..7aa4108428 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -6,7 +6,7 @@ using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using System; -using System.Diagnostics; +using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Replays; @@ -64,9 +64,9 @@ namespace osu.Game.Rulesets.Osu.Replays { buttonIndex = 0; - AddFrameToReplay(new ReplayFrame(-100000, 256, 500, ReplayButtonState.None)); - AddFrameToReplay(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, 256, 500, ReplayButtonState.None)); - AddFrameToReplay(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, 256, 192, ReplayButtonState.None)); + AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500))); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500))); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500))); for (int i = 0; i < Beatmap.HitObjects.Count; i++) { @@ -91,18 +91,18 @@ namespace osu.Game.Rulesets.Osu.Replays // Make the cursor stay at a hitObject as long as possible (mainly for autopilot). if (h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Miss) > endTime + h.HitWindows.HalfWindowFor(HitResult.Meh) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new OsuReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + if (!(h is Spinner)) AddFrameToReplay(new OsuReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } else if (h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh) > endTime + h.HitWindows.HalfWindowFor(HitResult.Meh) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new OsuReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + if (!(h is Spinner)) AddFrameToReplay(new OsuReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } else if (h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh) > endTime + h.HitWindows.HalfWindowFor(HitResult.Meh) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new OsuReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + if (!(h is Spinner)) AddFrameToReplay(new OsuReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } } @@ -118,9 +118,9 @@ namespace osu.Game.Rulesets.Osu.Replays // TODO: Shouldn't the spinner always spin in the same direction? if (h is Spinner) { - calcSpinnerStartPosAndDirection(Frames[Frames.Count - 1].Position, out startPosition, out spinnerDirection); + calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[Frames.Count - 1]).Position, out startPosition, out spinnerDirection); - Vector2 spinCentreOffset = SPINNER_CENTRE - Frames[Frames.Count - 1].Position; + Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[Frames.Count - 1]).Position; if (spinCentreOffset.Length > SPIN_RADIUS) { @@ -192,13 +192,13 @@ namespace osu.Game.Rulesets.Osu.Replays private void moveToHitObject(OsuHitObject h, Vector2 targetPos, Easing easing) { - ReplayFrame lastFrame = Frames[Frames.Count - 1]; + OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[Frames.Count - 1]; // Wait until Auto could "see and react" to the next note. double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime); if (waitTime > lastFrame.Time) { - lastFrame = new ReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState); + lastFrame = new OsuReplayFrame(waitTime, lastFrame.Position) { Actions = lastFrame.Actions }; AddFrameToReplay(lastFrame); } @@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Replays for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay) { Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing); - AddFrameToReplay(new ReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState)); + AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions }); } buttonIndex = 0; @@ -231,14 +231,14 @@ namespace osu.Game.Rulesets.Osu.Replays { // Time to insert the first frame which clicks the object // Here we mainly need to determine which button to use - ReplayButtonState button = buttonIndex % 2 == 0 ? ReplayButtonState.Left1 : ReplayButtonState.Right1; + var action = buttonIndex % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton; - ReplayFrame startFrame = new ReplayFrame(h.StartTime, startPosition.X, startPosition.Y, button); + var startFrame = new OsuReplayFrame(h.StartTime, new Vector2(startPosition.X, startPosition.Y), action); // TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime. double hEndTime = ((h as IHasEndTime)?.EndTime ?? h.StartTime) + KEY_UP_DELAY; int endDelay = h is Spinner ? 1 : 0; - ReplayFrame endFrame = new ReplayFrame(hEndTime + endDelay, h.StackedEndPosition.X, h.StackedEndPosition.Y, ReplayButtonState.None); + var endFrame = new OsuReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y)); // Decrement because we want the previous frame, not the next one int index = FindInsertionIndex(startFrame) - 1; @@ -248,19 +248,18 @@ namespace osu.Game.Rulesets.Osu.Replays // Do we have a previous frame? No need to check for < replay.Count since we decremented! if (index >= 0) { - ReplayFrame previousFrame = Frames[index]; - var previousButton = previousFrame.ButtonState; + var previousFrame = (OsuReplayFrame)Frames[index]; + var previousActions = previousFrame.Actions; // If a button is already held, then we simply alternate - if (previousButton != ReplayButtonState.None) + if (previousActions.Any()) { - Debug.Assert(previousButton != (ReplayButtonState.Left1 | ReplayButtonState.Right1), "Previous button state was not Left1 nor Right1 despite only using those two states."); - // Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button. - if (previousButton == button) + if (previousActions.Contains(action)) { - button = (ReplayButtonState.Left1 | ReplayButtonState.Right1) & ~button; - startFrame.ButtonState = button; + action = action == OsuAction.LeftButton ? OsuAction.RightButton : OsuAction.LeftButton; + startFrame.Actions.Clear(); + startFrame.Actions.Add(action); } // We always follow the most recent slider / spinner, so remove any other frames that occur while it exists. @@ -272,9 +271,14 @@ namespace osu.Game.Rulesets.Osu.Replays // 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) { + var frame = (OsuReplayFrame)Frames[j]; + // Don't affect frames which stop pressing a button! - if (j < Frames.Count - 1 || Frames[j].ButtonState == previousButton) - Frames[j].ButtonState = button; + if (j < Frames.Count - 1 || frame.Actions.SequenceEqual(previousActions)) + { + frame.Actions.Clear(); + frame.Actions.Add(action); + } } } } @@ -298,16 +302,15 @@ namespace osu.Game.Rulesets.Osu.Replays t = ApplyModsToTime(j - h.StartTime) * spinnerDirection; Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); - AddFrameToReplay(new ReplayFrame((int)j, pos.X, pos.Y, button)); + AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action)); } t = ApplyModsToTime(s.EndTime - h.StartTime) * spinnerDirection; Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); - AddFrameToReplay(new ReplayFrame(s.EndTime, endPosition.X, endPosition.Y, button)); + AddFrameToReplay(new OsuReplayFrame(s.EndTime, new Vector2(endPosition.X, endPosition.Y), action)); - endFrame.MouseX = endPosition.X; - endFrame.MouseY = endPosition.Y; + endFrame.Position = endPosition; } else if (h is Slider) { @@ -316,10 +319,10 @@ namespace osu.Game.Rulesets.Osu.Replays for (double j = FrameDelay; j < s.Duration; j += FrameDelay) { Vector2 pos = s.StackedPositionAt(j / s.Duration); - AddFrameToReplay(new ReplayFrame(h.StartTime + j, pos.X, pos.Y, button)); + AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action)); } - AddFrameToReplay(new ReplayFrame(s.EndTime, s.StackedEndPosition.X, s.StackedEndPosition.Y, button)); + AddFrameToReplay(new OsuReplayFrame(s.EndTime, new Vector2(s.StackedEndPosition.X, s.StackedEndPosition.Y), action)); } // 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! diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs new file mode 100644 index 0000000000..bcdfe07417 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2007-2018 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.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Rulesets.Replays.Types; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Replays +{ + public class OsuReplayFrame : ReplayFrame, IConvertibleReplayFrame + { + public Vector2 Position; + public List Actions = new List(); + + public OsuReplayFrame() + { + } + + public OsuReplayFrame(double time, Vector2 position, params OsuAction[] actions) + : base(time) + { + Position = position; + Actions.AddRange(actions); + } + + public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + { + Position = legacyFrame.Position; + if (legacyFrame.MouseLeft) Actions.Add(OsuAction.LeftButton); + if (legacyFrame.MouseRight) Actions.Add(OsuAction.RightButton); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs index 63c9111190..0a61b0f199 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs @@ -2,32 +2,42 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; using osu.Framework.Input; +using osu.Framework.MathUtils; using osu.Game.Rulesets.Replays; using OpenTK; namespace osu.Game.Rulesets.Osu.Replays { - public class OsuReplayInputHandler : FramedReplayInputHandler + public class OsuReplayInputHandler : FramedReplayInputHandler { public OsuReplayInputHandler(Replay replay) : base(replay) { } + protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any(); + + protected Vector2? Position + { + get + { + if (!HasFrames) + return null; + + return Interpolation.ValueAt(CurrentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time); + } + } + public override List GetPendingStates() { - List actions = new List(); - - if (CurrentFrame?.MouseLeft ?? false) actions.Add(OsuAction.LeftButton); - if (CurrentFrame?.MouseRight ?? false) actions.Add(OsuAction.RightButton); - return new List { new ReplayState { Mouse = new ReplayMouseState(ToScreenSpace(Position ?? Vector2.Zero)), - PressedActions = actions + PressedActions = CurrentFrame.Actions } }; } diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json new file mode 100644 index 0000000000..b82fddbe79 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -0,0 +1,124 @@ +{ + "Mappings": [{ + "StartTime": 500, + "Objects": [{ + "StartTime": 500, + "EndTime": 2500, + "StartX": 96, + "StartY": 192, + "EndX": 96, + "EndY": 192 + }] + }, + { + "StartTime": 3000, + "Objects": [{ + "StartTime": 3000, + "EndTime": 4000, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 4500, + "Objects": [{ + "StartTime": 4500, + "EndTime": 5500, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 6000, + "Objects": [{ + "StartTime": 6000, + "EndTime": 6500, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 7000, + "Objects": [{ + "StartTime": 7000, + "EndTime": 8000, + "StartX": 256, + "StartY": 128, + "EndX": 256, + "EndY": 128 + }] + }, + { + "StartTime": 8500, + "Objects": [{ + "StartTime": 8500, + "EndTime": 10999, + "StartX": 32, + "StartY": 192, + "EndX": 508.166229, + "EndY": 153.299271 + }] + }, + { + "StartTime": 11500, + "Objects": [{ + "StartTime": 11500, + "EndTime": 12000, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 12500, + "Objects": [{ + "StartTime": 12500, + "EndTime": 16500, + "StartX": 512, + "StartY": 320, + "EndX": 291.1977, + "EndY": 40.799427 + }] + }, + { + "StartTime": 17000, + "Objects": [{ + "StartTime": 17000, + "EndTime": 18000, + "StartX": 256, + "StartY": 256, + "EndX": 256, + "EndY": 256 + }] + }, + { + "StartTime": 18500, + "Objects": [{ + "StartTime": 18500, + "EndTime": 19450, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 19875, + "Objects": [{ + "StartTime": 19875, + "EndTime": 23874, + "StartX": 216, + "StartY": 231, + "EndX": 408.720825, + "EndY": 339.810455 + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic.osu new file mode 100644 index 0000000000..40b4409760 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json new file mode 100644 index 0000000000..7fe038658c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json @@ -0,0 +1,13 @@ +{ + "Mappings": [{ + "StartTime": 118858, + "Objects": [{ + "StartTime": 118858, + "EndTime": 119088, + "StartX": 219, + "StartY": 215, + "EndX": 239.6507, + "EndY": 29.1437378 + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu new file mode 100644 index 0000000000..8c3edc9571 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu @@ -0,0 +1,15 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4.2 +OverallDifficulty:9 +ApproachRate:9.8 +SliderMultiplier:1.87 +SliderTickRate:1 + +[TimingPoints] +49051,230.769230769231,4,2,1,15,1,0 + +[HitObjects] +219,215,118858,2,0,P|224:170|244:-10,1,187,8|2,0:0|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 67b96f1fd9..d41331e3bd 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -68,6 +68,8 @@ namespace osu.Game.Rulesets.Osu.Scoring score.Statistics[HitResult.Miss] = scoreResultCounts.GetOrDefault(HitResult.Miss); } + private const double harshness = 0.01; + protected override void OnNewJudgement(Judgement judgement) { base.OnNewJudgement(judgement); @@ -83,15 +85,15 @@ namespace osu.Game.Rulesets.Osu.Scoring switch (judgement.Result) { case HitResult.Great: - Health.Value += (10.2 - hpDrainRate) * 0.02; + Health.Value += (10.2 - hpDrainRate) * harshness; break; case HitResult.Good: - Health.Value += (8 - hpDrainRate) * 0.02; + Health.Value += (8 - hpDrainRate) * harshness; break; case HitResult.Meh: - Health.Value += (4 - hpDrainRate) * 0.02; + Health.Value += (4 - hpDrainRate) * harshness; break; /*case HitResult.SliderTick: @@ -99,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break;*/ case HitResult.Miss: - Health.Value -= hpDrainRate * 0.04; + Health.Value -= hpDrainRate * (harshness * 2); break; } } diff --git a/osu.Game.Rulesets.Osu/Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu/Tests/OsuBeatmapConversionTest.cs new file mode 100644 index 0000000000..59c59dc0e3 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Tests/OsuBeatmapConversionTest.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class OsuBeatmapConversionTest : BeatmapConversionTest + { + protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; + + [TestCase("basic")] + [TestCase("colinear-perfect-curve")] + public new void Test(string name) + { + base.Test(name); + } + + protected override IEnumerable CreateConvertValue(HitObject hitObject) + { + var startPosition = (hitObject as IHasPosition)?.Position ?? new Vector2(256, 192); + var endPosition = (hitObject as Slider)?.EndPosition ?? startPosition; + + yield return new ConvertValue + { + StartTime = hitObject.StartTime, + EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime, + StartX = startPosition.X, + StartY = startPosition.Y, + EndX = endPosition.X, + EndY = endPosition.Y + }; + } + + protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new OsuBeatmapConverter(); + } + + public struct ConvertValue : IEquatable + { + /// + /// A sane value to account for osu!stable using ints everwhere. + /// + private const double conversion_lenience = 2; + + public double StartTime; + public double EndTime; + public float StartX; + public float StartY; + public float EndX; + public float EndY; + + public bool Equals(ConvertValue other) + => Precision.AlmostEquals(StartTime, other.StartTime) + && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) + && Precision.AlmostEquals(StartX, other.StartX) + && Precision.AlmostEquals(StartY, other.StartY, conversion_lenience) + && Precision.AlmostEquals(EndX, other.EndX, conversion_lenience) + && Precision.AlmostEquals(EndY, other.EndY, conversion_lenience); + } +} diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs index d8bb7f88c7..f40d9c05d1 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs @@ -16,10 +16,12 @@ using System.Collections.Generic; using System; using osu.Game.Rulesets.Mods; using System.Linq; +using NUnit.Framework; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Tests { + [TestFixture] public class TestCaseHitCircle : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs index 563df631a1..f030c6db60 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Tests { + [TestFixture] public class TestCaseHitCircleHidden : TestCaseHitCircle { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); diff --git a/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs index 32339aaf3c..b6dca3f1cb 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs @@ -1,8 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; + namespace osu.Game.Rulesets.Osu.Tests { + [TestFixture] public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints { public TestCasePerformancePoints() diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs index 90a0a450a7..b68f59877b 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs @@ -15,6 +15,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Game.Rulesets.Mods; using System.Linq; +using NUnit.Framework; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Osu.Tests { + [TestFixture] public class TestCaseSlider : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] @@ -118,8 +120,8 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-(distance / 2), 0), - new Vector2(distance / 2, 0), + Vector2.Zero, + new Vector2(distance, 0), }, Distance = distance, RepeatCount = repeats, @@ -139,9 +141,9 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0), - new Vector2(0, 200), - new Vector2(200, 0) + Vector2.Zero, + new Vector2(200, 200), + new Vector2(400, 0) }, Distance = 600, RepeatCount = repeats, @@ -163,12 +165,12 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0), - new Vector2(-50, 75), - new Vector2(0, 100), - new Vector2(100, -200), + Vector2.Zero, + new Vector2(150, 75), new Vector2(200, 0), - new Vector2(230, 0) + new Vector2(300, -200), + new Vector2(400, 0), + new Vector2(430, 0) }, Distance = 793.4417, RepeatCount = repeats, @@ -190,11 +192,11 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0), - new Vector2(-50, 75), - new Vector2(0, 100), - new Vector2(100, -200), - new Vector2(230, 0) + Vector2.Zero, + new Vector2(150, 75), + new Vector2(200, 100), + new Vector2(300, -200), + new Vector2(430, 0) }, Distance = 480, RepeatCount = repeats, @@ -216,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(0, 0), + Vector2.Zero, new Vector2(-200, 0), new Vector2(0, 0), new Vector2(0, -200), @@ -247,10 +249,10 @@ namespace osu.Game.Rulesets.Osu.Tests CurveType = CurveType.Catmull, ControlPoints = new List { - new Vector2(-100, 0), - new Vector2(-50, -50), - new Vector2(50, 50), - new Vector2(100, 0) + Vector2.Zero, + new Vector2(50, -50), + new Vector2(150, 50), + new Vector2(200, 0) }, Distance = 300, RepeatCount = repeats, diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs index eba0ebc642..57b719464f 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Tests { + [TestFixture] public class TestCaseSliderHidden : TestCaseSlider { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs index c054f6aa28..d3620bcbda 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -16,6 +17,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { + [TestFixture] public class TestCaseSpinner : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs index a3bbce5b39..75b3b4c763 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Tests { + [TestFixture] public class TestCaseSpinnerHidden : TestCaseSpinner { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 7f8cbce78e..98a8096678 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.UI public class OsuPlayfield : Playfield { private readonly Container approachCircles; - private readonly Container judgementLayer; + private readonly JudgementContainer judgementLayer; private readonly ConnectionRenderer connectionLayer; // Todo: This should not be a thing, but is currently required for the editor @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.UI RelativeSizeAxes = Axes.Both, Depth = 2, }, - judgementLayer = new Container + judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both, Depth = 1, @@ -75,16 +75,13 @@ namespace osu.Game.Rulesets.Osu.UI private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) { - var osuJudgement = (OsuJudgement)judgement; - var osuObject = (OsuHitObject)judgedObject.HitObject; - if (!judgedObject.DisplayJudgement) return; - DrawableOsuJudgement explosion = new DrawableOsuJudgement(osuJudgement) + DrawableOsuJudgement explosion = new DrawableOsuJudgement(judgement, judgedObject) { Origin = Anchor.Centre, - Position = osuObject.StackedEndPosition + osuJudgement.PositionOffset + Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition + ((OsuJudgement)judgement).PositionOffset }; judgementLayer.Add(explosion); diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index 2af381dd71..b825ba73b7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Input; using OpenTK; using osu.Game.Beatmaps; +using osu.Game.Input.Handlers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; @@ -48,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.UI return null; } - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuReplayInputHandler(replay); + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuReplayInputHandler(replay); protected override Vector2 GetAspectAdjustedSize() { diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 7838fb7707..92cac71ad3 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -64,6 +64,10 @@ + + + + @@ -83,6 +87,7 @@ + @@ -92,6 +97,7 @@ + @@ -121,7 +127,9 @@ + + @@ -167,6 +175,12 @@ + + + + + + diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs index 5493a5029b..afa3d162f4 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs @@ -2,10 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Audio { @@ -14,7 +13,9 @@ namespace osu.Game.Rulesets.Taiko.Audio private readonly ControlPointInfo controlPoints; private readonly Dictionary mappings = new Dictionary(); - public DrumSampleMapping(ControlPointInfo controlPoints, AudioManager audio) + public readonly List Sounds = new List(); + + public DrumSampleMapping(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; @@ -27,20 +28,34 @@ namespace osu.Game.Rulesets.Taiko.Audio foreach (var s in samplePoints) { + var centre = s.GetSampleInfo(); + var rim = s.GetSampleInfo(SampleInfo.HIT_CLAP); + + // todo: this is ugly + centre.Namespace = "taiko"; + rim.Namespace = "taiko"; + mappings[s.Time] = new DrumSample { - Centre = s.GetSampleInfo().GetChannel(audio.Sample, "Taiko"), - Rim = s.GetSampleInfo(SampleInfo.HIT_CLAP).GetChannel(audio.Sample, "Taiko") + Centre = addSound(centre), + Rim = addSound(rim) }; } } + private SkinnableSound addSound(SampleInfo sampleInfo) + { + var drawable = new SkinnableSound(sampleInfo); + Sounds.Add(drawable); + return drawable; + } + public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time]; public class DrumSample { - public SampleChannel Centre; - public SampleChannel Rim; + public SkinnableSound Centre; + public SkinnableSound Rim; } } } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index e5fe288f20..9d6b5b5ce4 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -101,16 +101,16 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // The duration of the taiko hit object double taikoDuration = distance / taikoVelocity; - // For some reason, old osu! always uses speedAdjustment to determine the taiko velocity, but - // only uses it to determine osu! velocity if beatmap version < 8. Let's account for that here. - if (beatmap.BeatmapInfo.BeatmapVersion >= 8) - speedAdjustedBeatLength *= speedAdjustment; - // The velocity of the osu! hit object - calculated as the velocity of a slider double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; + // osu-stable always uses the speed-adjusted beatlength to determine the velocities, but + // only uses it for tick rate if beatmap version < 8 + if (beatmap.BeatmapInfo.BeatmapVersion >= 8) + speedAdjustedBeatLength *= speedAdjustment; + // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 29d464f614..f98e6b936e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -82,8 +82,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; int countHit = NestedHitObjects.Count(o => o.IsHit); - - if (countHit > HitObject.RequiredGoodHits) + if (countHit >= HitObject.RequiredGoodHits) { AddJudgement(new TaikoJudgement { Result = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good }); if (HitObject.IsStrong) diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 002159439d..1a556fe01d 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -35,15 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Replays { bool hitButton = true; - Frames.Add(new TaikoReplayFrame(-100000, ReplayButtonState.None)); - Frames.Add(new TaikoReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, ReplayButtonState.None)); + Frames.Add(new TaikoReplayFrame(-100000)); + Frames.Add(new TaikoReplayFrame(Beatmap.HitObjects[0].StartTime - 1000)); for (int i = 0; i < Beatmap.HitObjects.Count; i++) { TaikoHitObject h = Beatmap.HitObjects[i]; - ReplayButtonState button; - IHasEndTime endTimeData = h as IHasEndTime; double endTime = endTimeData?.EndTime ?? h.StartTime; @@ -59,24 +57,26 @@ namespace osu.Game.Rulesets.Taiko.Replays double hitRate = Math.Min(swell_hit_speed, swell.Duration / req); for (double j = h.StartTime; j < endTime; j += hitRate) { + TaikoAction action; + switch (d) { default: case 0: - button = ReplayButtonState.Left1; + action = TaikoAction.LeftCentre; break; case 1: - button = ReplayButtonState.Right1; + action = TaikoAction.LeftRim; break; case 2: - button = ReplayButtonState.Left2; + action = TaikoAction.RightCentre; break; case 3: - button = ReplayButtonState.Right2; + action = TaikoAction.RightRim; break; } - Frames.Add(new TaikoReplayFrame(j, button)); + Frames.Add(new TaikoReplayFrame(j, action)); d = (d + 1) % 4; if (++count == req) break; @@ -86,39 +86,39 @@ namespace osu.Game.Rulesets.Taiko.Replays { foreach (var tick in drumRoll.NestedHitObjects.OfType()) { - Frames.Add(new TaikoReplayFrame(tick.StartTime, hitButton ? ReplayButtonState.Right1 : ReplayButtonState.Right2)); + Frames.Add(new TaikoReplayFrame(tick.StartTime, hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre)); hitButton = !hitButton; } } else if (hit != null) { + TaikoAction[] actions; + if (hit is CentreHit) { - if (h.IsStrong) - button = ReplayButtonState.Right1 | ReplayButtonState.Right2; - else - button = hitButton ? ReplayButtonState.Right1 : ReplayButtonState.Right2; + actions = h.IsStrong + ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre } + : new[] { hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre }; } else { - if (h.IsStrong) - button = ReplayButtonState.Left1 | ReplayButtonState.Left2; - else - button = hitButton ? ReplayButtonState.Left1 : ReplayButtonState.Left2; + actions = h.IsStrong + ? new[] { TaikoAction.LeftRim, TaikoAction.RightRim } + : new[] { hitButton ? TaikoAction.LeftRim : TaikoAction.RightRim }; } - Frames.Add(new TaikoReplayFrame(h.StartTime, button)); + Frames.Add(new TaikoReplayFrame(h.StartTime, actions)); } else throw new InvalidOperationException("Unknown hit object type."); - Frames.Add(new TaikoReplayFrame(endTime + KEY_UP_DELAY, ReplayButtonState.None)); + Frames.Add(new TaikoReplayFrame(endTime + KEY_UP_DELAY)); if (i < Beatmap.HitObjects.Count - 1) { double waitTime = Beatmap.HitObjects[i + 1].StartTime - 1000; if (waitTime > endTime) - Frames.Add(new TaikoReplayFrame(waitTime, ReplayButtonState.None)); + Frames.Add(new TaikoReplayFrame(waitTime)); } hitButton = !hitButton; diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs index 05e10b6fce..c80bddc304 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs @@ -3,31 +3,20 @@ using osu.Game.Rulesets.Replays; using System.Collections.Generic; +using System.Linq; using osu.Framework.Input; namespace osu.Game.Rulesets.Taiko.Replays { - internal class TaikoFramedReplayInputHandler : FramedReplayInputHandler + internal class TaikoFramedReplayInputHandler : FramedReplayInputHandler { public TaikoFramedReplayInputHandler(Replay replay) : base(replay) { } - public override List GetPendingStates() - { - var actions = new List(); + protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any(); - if (CurrentFrame?.MouseRight1 == true) - actions.Add(TaikoAction.LeftCentre); - if (CurrentFrame?.MouseRight2 == true) - actions.Add(TaikoAction.RightCentre); - if (CurrentFrame?.MouseLeft1 == true) - actions.Add(TaikoAction.LeftRim); - if (CurrentFrame?.MouseLeft2 == true) - actions.Add(TaikoAction.RightRim); - - return new List { new ReplayState { PressedActions = actions } }; - } + public override List GetPendingStates() => new List { new ReplayState { PressedActions = CurrentFrame.Actions } }; } } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs index 0c60cdc109..6cd63f6c70 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs @@ -1,17 +1,34 @@ // Copyright (c) 2007-2018 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.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Taiko.Replays { - public class TaikoReplayFrame : ReplayFrame + public class TaikoReplayFrame : ReplayFrame, IConvertibleReplayFrame { - public override bool IsImportant => MouseLeft || MouseRight; + public List Actions = new List(); - public TaikoReplayFrame(double time, ReplayButtonState buttons) - : base(time, null, null, buttons) + public TaikoReplayFrame() { } + + public TaikoReplayFrame(double time, params TaikoAction[] actions) + : base(time) + { + Actions.AddRange(actions); + } + + public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + { + if (legacyFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim); + if (legacyFrame.MouseRight2) Actions.Add(TaikoAction.RightRim); + if (legacyFrame.MouseLeft1) Actions.Add(TaikoAction.LeftCentre); + if (legacyFrame.MouseLeft2) Actions.Add(TaikoAction.RightCentre); + } } } diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic-expected-conversion.json new file mode 100644 index 0000000000..5c9310fec7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -0,0 +1,209 @@ +{ + "Mappings": [{ + "StartTime": 500, + "Objects": [{ + "StartTime": 500, + "EndTime": 2499, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 3000, + "Objects": [{ + "StartTime": 3000, + "EndTime": 4000, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 4500, + "Objects": [{ + "StartTime": 4500, + "EndTime": 5500, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 6000, + "Objects": [{ + "StartTime": 6000, + "EndTime": 6500, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 7000, + "Objects": [{ + "StartTime": 7000, + "EndTime": 7000, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 7249, + "EndTime": 7249, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 7499, + "EndTime": 7499, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 7749, + "EndTime": 7749, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 7999, + "EndTime": 7999, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 8500, + "Objects": [{ + "StartTime": 8500, + "EndTime": 10999, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 11500, + "Objects": [{ + "StartTime": 11500, + "EndTime": 12000, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 12500, + "Objects": [{ + "StartTime": 12500, + "EndTime": 16499, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 17000, + "Objects": [{ + "StartTime": 17000, + "EndTime": 17000, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 17249, + "EndTime": 17249, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 17499, + "EndTime": 17499, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 17749, + "EndTime": 17749, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 17999, + "EndTime": 17999, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 18500, + "Objects": [{ + "StartTime": 18500, + "EndTime": 19450, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 19875, + "Objects": [{ + "StartTime": 19875, + "EndTime": 23874, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic.osu new file mode 100644 index 0000000000..40b4409760 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-expected-conversion.json new file mode 100644 index 0000000000..fc7d466c1b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-expected-conversion.json @@ -0,0 +1,87 @@ +{ + "Mappings": [{ + "StartTime": 6590, + "Objects": [{ + "StartTime": 6590, + "EndTime": 8320, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 8436, + "Objects": [{ + "StartTime": 8436, + "EndTime": 10166, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 10282, + "Objects": [{ + "StartTime": 10282, + "EndTime": 12012, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 12128, + "Objects": [{ + "StartTime": 12128, + "EndTime": 13858, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 41666, + "Objects": [{ + "StartTime": 41666, + "EndTime": 42589, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 62666, + "Objects": [{ + "StartTime": 62666, + "EndTime": 63127, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 208743, + "Objects": [{ + "StartTime": 208743, + "EndTime": 209204, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll.osu new file mode 100644 index 0000000000..4c493b47d4 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll.osu @@ -0,0 +1,25 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4.2 +OverallDifficulty:9 +ApproachRate:9.8 +SliderMultiplier:1.87 +SliderTickRate:1 + +[TimingPoints] +6590,461.538461538462,4,2,2,15,1,0 +6590,-200,4,2,2,15,0,0 +49051,230.769230769231,4,2,1,15,1,0 +62666,-200,4,2,1,60,0,0 +197666,-100,4,2,1,85,0,1 + +[HitObjects] +88,104,6590,6,0,B|176:156|256:108|256:108|336:60|423:112,1,350.625,6|0,0:0|0:0,0:0:0:0: +396,213,8436,2,0,P|277:247|376:172,1,350.625,6|0,0:0|0:0,0:0:0:0: +472,220,10282,2,0,P|456:288|220:300,1,350.625,6|0,0:0|0:0,0:0:0:0: +277,200,12128,2,0,P|398:225|276:244,1,350.625,6|0,0:0|0:0,0:0:0:0: +268,229,41666,2,0,L|473:210,1,187,2|2,0:0|0:0,0:0:0:0: +133,342,62666,2,0,B|132:316|132:316|128:316|128:316|130:295|130:295|126:296|126:296|129:275|129:275|125:275|125:275|127:254|127:254|123:255|123:255|125:234|125:234|121:234|121:234|123:213|123:213|119:214|119:214|121:193|121:193|118:193|118:193|118:172,1,187,8|8,0:0|0:0,0:0:0:0: +481,338,208743,6,0,P|492:262|383:195,2,187,2|8|2,0:0|0:0|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 50cc80db50..0a9719f27b 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -10,6 +10,8 @@ using osu.Game.Rulesets.UI; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Taiko.Replays; namespace osu.Game.Rulesets.Taiko { @@ -101,7 +103,9 @@ namespace osu.Game.Rulesets.Taiko public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap); - public override int LegacyID => 1; + public override int? LegacyID => 1; + + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); public TaikoRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) diff --git a/osu.Game.Rulesets.Taiko/Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko/Tests/TaikoBeatmapConversionTest.cs new file mode 100644 index 0000000000..385e041ace --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Tests/TaikoBeatmapConversionTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TaikoBeatmapConversionTest : BeatmapConversionTest + { + protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; + + private bool isForCurrentRuleset; + + [NonParallelizable] + [TestCase("basic", false), Ignore("See: https://github.com/ppy/osu/issues/2152")] + [TestCase("slider-generating-drumroll", false)] + public void Test(string name, bool isForCurrentRuleset) + { + this.isForCurrentRuleset = isForCurrentRuleset; + base.Test(name); + } + + protected override IEnumerable CreateConvertValue(HitObject hitObject) + { + yield return new ConvertValue + { + StartTime = hitObject.StartTime, + EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime, + IsRim = hitObject is RimHit, + IsCentre = hitObject is CentreHit, + IsDrumRoll = hitObject is DrumRoll, + IsSwell = hitObject is Swell, + IsStrong = ((TaikoHitObject)hitObject).IsStrong + }; + } + + protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new TaikoBeatmapConverter(isForCurrentRuleset); + } + + public struct ConvertValue : IEquatable + { + /// + /// A sane value to account for osu!stable using ints everwhere. + /// + private const float conversion_lenience = 2; + + public double StartTime; + public double EndTime; + public bool IsRim; + public bool IsCentre; + public bool IsDrumRoll; + public bool IsSwell; + public bool IsStrong; + + public bool Equals(ConvertValue other) + => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) + && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) + && IsRim == other.IsRim + && IsCentre == other.IsCentre + && IsDrumRoll == other.IsDrumRoll + && IsSwell == other.IsSwell + && IsStrong == other.IsStrong; + } +} diff --git a/osu.Game.Rulesets.Taiko/Tests/TestCaseInputDrum.cs b/osu.Game.Rulesets.Taiko/Tests/TestCaseInputDrum.cs index 437237661c..80721271d6 100644 --- a/osu.Game.Rulesets.Taiko/Tests/TestCaseInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Tests/TestCaseInputDrum.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,6 +15,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { + [TestFixture] public class TestCaseInputDrum : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs index a291bc2eea..f6b0ceb7bd 100644 --- a/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs +++ b/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs @@ -1,8 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; + namespace osu.Game.Rulesets.Taiko.Tests { + [TestFixture] public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints { public TestCasePerformancePoints() diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index c0e8bd1b5a..6274232ffd 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -15,17 +15,14 @@ namespace osu.Game.Rulesets.Taiko.UI /// public class DrawableTaikoJudgement : DrawableJudgement { - public readonly DrawableHitObject JudgedObject; - /// /// Creates a new judgement text. /// /// The object which is being judged. /// The judgement to visualise. - public DrawableTaikoJudgement(DrawableHitObject judgedObject, Judgement judgement) - : base(judgement) + public DrawableTaikoJudgement(Judgement judgement, DrawableHitObject judgedObject) + : base(judgement, judgedObject) { - JudgedObject = judgedObject; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 98f20fd558..b918f495fc 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -4,7 +4,6 @@ using System; using OpenTK; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -34,9 +33,9 @@ namespace osu.Game.Rulesets.Taiko.UI } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { - var sampleMappings = new DrumSampleMapping(controlPoints, audio); + var sampleMappings = new DrumSampleMapping(controlPoints); Children = new Drawable[] { @@ -63,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.UI CentreAction = TaikoAction.RightCentre } }; + + AddRangeInternal(sampleMappings.Sounds); } /// diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 49c87f7480..75aaceaecb 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -16,6 +16,7 @@ using System.Linq; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Taiko.UI @@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; - private readonly Container judgementContainer; + private readonly JudgementContainer judgementContainer; protected override Container Content => content; private readonly Container content; @@ -131,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.UI Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, Blending = BlendingMode.Additive }, - judgementContainer = new Container + judgementContainer = new JudgementContainer { Name = "Judgements", RelativeSizeAxes = Axes.Y, @@ -227,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.UI { if (judgedObject.DisplayJudgement && judgementContainer.FirstOrDefault(j => j.JudgedObject == judgedObject) == null) { - judgementContainer.Add(new DrawableTaikoJudgement(judgedObject, judgement) + judgementContainer.Add(new DrawableTaikoJudgement(judgement, judgedObject) { Anchor = judgement.IsHit ? Anchor.TopLeft : Anchor.CentreLeft, Origin = judgement.IsHit ? Anchor.BottomCentre : Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index fd31f738ee..eb282c53ca 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.Taiko.Replays; using OpenTK; using System.Linq; using osu.Framework.Input; +using osu.Game.Input.Handlers; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Taiko.UI @@ -133,6 +134,6 @@ namespace osu.Game.Rulesets.Taiko.UI return null; } - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); } } diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index db2db9fff1..8f0aa88e62 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -112,6 +112,7 @@ + @@ -145,6 +146,12 @@ + + + + + + diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 21bbc4993c..ab10da2cd1 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapGeneral() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(4, difficulty.CircleSize); Assert.AreEqual(8, difficulty.OverallDifficulty); Assert.AreEqual(9, difficulty.ApproachRate); - Assert.AreEqual(1.8f, difficulty.SliderMultiplier); + Assert.AreEqual(1.8, difficulty.SliderMultiplier); Assert.AreEqual(2, difficulty.SliderTickRate); } } @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapEvents() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { @@ -128,7 +128,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapTimingPoints() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { @@ -187,7 +187,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapHitObjects() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 186bd44640..89d96c774e 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(4, difficulty.CircleSize); Assert.AreEqual(8, difficulty.OverallDifficulty); Assert.AreEqual(9, difficulty.ApproachRate); - Assert.AreEqual(1.8f, difficulty.SliderMultiplier); + Assert.AreEqual(1.8, difficulty.SliderMultiplier); Assert.AreEqual(2, difficulty.SliderTickRate); } @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var sr = new StreamReader(stream)) { - var legacyDecoded = new LegacyBeatmapDecoder().DecodeBeatmap(sr); + var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.DecodeBeatmap(sr); using (var ms = new MemoryStream()) using (var sw = new StreamWriter(ms)) using (var sr2 = new StreamReader(ms)) diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 7a1c6d9b89..1f7246a119 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); Assert.AreEqual("Deif", meta.AuthorString); Assert.AreEqual("machinetop_background.jpg", meta.BackgroundFile); - Assert.AreEqual(164471, meta.PreviewTime); + Assert.AreEqual(164471 + LegacyBeatmapDecoder.UniversalOffset, meta.PreviewTime); Assert.AreEqual(string.Empty, meta.Source); Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", meta.Tags); Assert.AreEqual("Renatus", meta.Title); diff --git a/osu.Game.Tests/Visual/TestCaseAllPlayers.cs b/osu.Game.Tests/Visual/TestCaseAllPlayers.cs index 912dbc4056..e633d121ca 100644 --- a/osu.Game.Tests/Visual/TestCaseAllPlayers.cs +++ b/osu.Game.Tests/Visual/TestCaseAllPlayers.cs @@ -1,8 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; + namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseAllPlayers : TestCasePlayer { } diff --git a/osu.Game.Tests/Visual/TestCaseBeatSyncedContainer.cs b/osu.Game.Tests/Visual/TestCaseBeatSyncedContainer.cs index f081d090c8..66cee634f5 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatSyncedContainer.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using NUnit.Framework; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -17,6 +18,7 @@ using osu.Framework.Lists; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBeatSyncedContainer : OsuTestCase { private readonly MusicController mc; diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs index 901d24e531..c68e548f44 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs @@ -6,20 +6,24 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Rulesets; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBeatmapCarousel : OsuTestCase { private TestBeatmapCarousel carousel; + private RulesetStore rulesets; public override IReadOnlyList RequiredTypes => new[] { @@ -44,8 +48,10 @@ namespace osu.Game.Tests.Visual private const int set_count = 5; [BackgroundDependencyLoader] - private void load() + private void load(RulesetStore rulesets) { + this.rulesets = rulesets; + Add(carousel = new TestBeatmapCarousel { RelativeSizeAxes = Axes.Both, @@ -73,6 +79,7 @@ namespace osu.Game.Tests.Visual testRemoveAll(); testEmptyTraversal(); testHiding(); + testSelectingFilteredRuleset(); } private void ensureRandomFetchSuccess() => @@ -207,6 +214,12 @@ namespace osu.Game.Tests.Visual checkVisibleItemCount(true, 0); AddAssert("Selection is null", () => currentSelection == null); + advanceSelection(true); + AddAssert("Selection is null", () => currentSelection == null); + + advanceSelection(false); + AddAssert("Selection is null", () => currentSelection == null); + AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); AddAssert("Selection is non-null", () => currentSelection != null); @@ -355,6 +368,41 @@ namespace osu.Game.Tests.Visual } } + private void testSelectingFilteredRuleset() + { + var testMixed = createTestBeatmapSet(set_count + 1); + AddStep("add mixed ruleset beatmapset", () => + { + for (int i = 0; i <= 2; i++) + { + testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i); + testMixed.Beatmaps[i].RulesetID = i; + } + + carousel.UpdateBeatmapSet(testMixed); + }); + AddStep("filter to ruleset 0", () => + carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); + AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); + AddAssert("unfiltered beatmap selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0])); + + AddStep("remove mixed set", () => + { + carousel.RemoveBeatmapSet(testMixed); + testMixed = null; + }); + var testSingle = createTestBeatmapSet(set_count + 2); + testSingle.Beatmaps.ForEach(b => + { + b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); + b.RulesetID = b.Ruleset.ID ?? 1; + }); + AddStep("add single ruleset beatmapset", () => carousel.UpdateBeatmapSet(testSingle)); + AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testSingle.Beatmaps[0], false)); + checkNoSelection(); + AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle)); + } + private BeatmapSetInfo createTestBeatmapSet(int id) { return new BeatmapSetInfo diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs index bde071c4a3..3ccdaa90d9 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using OpenTK; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -18,6 +19,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBeatmapInfoWedge : OsuTestCase { private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/TestCaseBeatmapSetOverlay.cs index ad85b3ed52..d9aedb7a5f 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapSetOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -12,6 +13,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBeatmapSetOverlay : OsuTestCase { private readonly BeatmapSetOverlay overlay; diff --git a/osu.Game.Tests/Visual/TestCaseBreadcrumbs.cs b/osu.Game.Tests/Visual/TestCaseBreadcrumbs.cs index 34abef7d76..20bdd6736c 100644 --- a/osu.Game.Tests/Visual/TestCaseBreadcrumbs.cs +++ b/osu.Game.Tests/Visual/TestCaseBreadcrumbs.cs @@ -1,11 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBreadcrumbs : OsuTestCase { public TestCaseBreadcrumbs() diff --git a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs b/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs index f9ed606080..51b8c61963 100644 --- a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs @@ -3,11 +3,13 @@ using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; -using osu.Game.Screens.Play.BreaksOverlay; using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBreakOverlay : OsuTestCase { private readonly BreakOverlay breakOverlay; diff --git a/osu.Game.Tests/Visual/TestCaseButtonSystem.cs b/osu.Game.Tests/Visual/TestCaseButtonSystem.cs index 61da76970e..93740593cb 100644 --- a/osu.Game.Tests/Visual/TestCaseButtonSystem.cs +++ b/osu.Game.Tests/Visual/TestCaseButtonSystem.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Shapes; @@ -9,6 +10,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseButtonSystem : OsuTestCase { public TestCaseButtonSystem() diff --git a/osu.Game.Tests/Visual/TestCaseChatLink.cs b/osu.Game.Tests/Visual/TestCaseChatLink.cs index 3a7be686e1..786fcb64ab 100644 --- a/osu.Game.Tests/Visual/TestCaseChatLink.cs +++ b/osu.Game.Tests/Visual/TestCaseChatLink.cs @@ -12,12 +12,14 @@ using osu.Game.Users; using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseChatLink : OsuTestCase { private readonly TestChatLineContainer textContainer; diff --git a/osu.Game.Tests/Visual/TestCaseContextMenu.cs b/osu.Game.Tests/Visual/TestCaseContextMenu.cs index 6098187dd6..45c12cf4af 100644 --- a/osu.Game.Tests/Visual/TestCaseContextMenu.cs +++ b/osu.Game.Tests/Visual/TestCaseContextMenu.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -13,6 +14,7 @@ using osu.Game.Graphics.Cursor; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseContextMenu : OsuTestCase { private const int start_time = 0; diff --git a/osu.Game.Tests/Visual/TestCaseCursors.cs b/osu.Game.Tests/Visual/TestCaseCursors.cs index 363f6b53f0..72e699c54b 100644 --- a/osu.Game.Tests/Visual/TestCaseCursors.cs +++ b/osu.Game.Tests/Visual/TestCaseCursors.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -16,6 +17,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseCursors : OsuTestCase { private readonly ManualInputManager inputManager; diff --git a/osu.Game.Tests/Visual/TestCaseDialogOverlay.cs b/osu.Game.Tests/Visual/TestCaseDialogOverlay.cs index d7fbf64664..e9512b29f7 100644 --- a/osu.Game.Tests/Visual/TestCaseDialogOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseDialogOverlay.cs @@ -1,12 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseDialogOverlay : OsuTestCase { public TestCaseDialogOverlay() diff --git a/osu.Game.Tests/Visual/TestCaseDirect.cs b/osu.Game.Tests/Visual/TestCaseDirect.cs index 8fa576135e..3f3dbb0bca 100644 --- a/osu.Game.Tests/Visual/TestCaseDirect.cs +++ b/osu.Game.Tests/Visual/TestCaseDirect.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -9,6 +10,7 @@ using osu.Game.Rulesets; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseDirect : OsuTestCase { private DirectOverlay direct; diff --git a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs index ec70253118..4268fd305e 100644 --- a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs +++ b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,6 +13,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseDrawableRoom : OsuTestCase { private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/TestCaseEditor.cs b/osu.Game.Tests/Visual/TestCaseEditor.cs index 37da41c228..c626ca8e7f 100644 --- a/osu.Game.Tests/Visual/TestCaseEditor.cs +++ b/osu.Game.Tests/Visual/TestCaseEditor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Screens.Edit; @@ -10,6 +11,7 @@ using osu.Game.Screens.Edit.Screens; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditor : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(Editor), typeof(EditorScreen) }; diff --git a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs index 76771ecf82..15bccac172 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs @@ -2,12 +2,14 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Screens.Edit.Screens.Compose; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorCompose : OsuTestCase { private readonly Random random; diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs index 8717f15311..d9850139cd 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Screens.Compose.RadioButtons; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorComposeRadioButtons : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(DrawableRadioButton) }; diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs index 6a47933a3c..d15ee32d8d 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using OpenTK; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -12,6 +13,7 @@ using osu.Game.Screens.Edit.Screens.Compose.Timeline; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorComposeTimeline : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(ScrollableTimeline), typeof(ScrollingTimelineContainer), typeof(BeatmapWaveformGraph), typeof(TimelineButton) }; diff --git a/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs b/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs index edfcde22b3..ee98fa087a 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -11,6 +12,7 @@ using osu.Game.Screens.Edit.Menus; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorMenuBar : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(EditorMenuBar), typeof(ScreenSelectionTabControl) }; diff --git a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs index 5e0c0e165c..8d12dfc517 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs @@ -3,25 +3,38 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Allocation; using OpenTK; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Layers.Selection; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Edit; +using osu.Game.Rulesets.Osu.Edit.Layers.Selection; +using osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorSelectionLayer : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(SelectionBox), typeof(SelectionLayer), - typeof(CaptureBox) + typeof(CaptureBox), + typeof(HitObjectComposer), + typeof(OsuHitObjectComposer), + typeof(HitObjectOverlayLayer), + typeof(OsuHitObjectOverlayLayer), + typeof(HitObjectOverlay), + typeof(HitCircleOverlay), + typeof(SliderOverlay), + typeof(SliderCircleOverlay) }; [BackgroundDependencyLoader] @@ -35,13 +48,13 @@ namespace osu.Game.Tests.Visual new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }, new Slider { + Position = new Vector2(128, 256), ControlPoints = new List { - new Vector2(128, 256), - new Vector2(344, 256), + Vector2.Zero, + new Vector2(216, 0), }, Distance = 400, - Position = new Vector2(128, 256), Velocity = 1, TickDistance = 100, Scale = 0.5f, diff --git a/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs index 8c8699fffa..26c8814bc4 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Framework.Graphics; @@ -14,6 +15,7 @@ using osu.Framework.Configuration; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorSummaryTimeline : OsuTestCase { private const int length = 60000; diff --git a/osu.Game.Tests/Visual/TestCaseGamefield.cs b/osu.Game.Tests/Visual/TestCaseGamefield.cs index 44f46dea18..80b3f9eb40 100644 --- a/osu.Game.Tests/Visual/TestCaseGamefield.cs +++ b/osu.Game.Tests/Visual/TestCaseGamefield.cs @@ -1,10 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseGamefield : OsuTestCase { protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/TestCaseGraph.cs b/osu.Game.Tests/Visual/TestCaseGraph.cs index 99184d4689..285a43707a 100644 --- a/osu.Game.Tests/Visual/TestCaseGraph.cs +++ b/osu.Game.Tests/Visual/TestCaseGraph.cs @@ -2,12 +2,14 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseGraph : OsuTestCase { public TestCaseGraph() diff --git a/osu.Game.Tests/Visual/TestCaseHistoricalSection.cs b/osu.Game.Tests/Visual/TestCaseHistoricalSection.cs index a7fc58f2b5..2e94baa9fc 100644 --- a/osu.Game.Tests/Visual/TestCaseHistoricalSection.cs +++ b/osu.Game.Tests/Visual/TestCaseHistoricalSection.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -13,6 +14,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseHistoricalSection : OsuTestCase { public override IReadOnlyList RequiredTypes => diff --git a/osu.Game.Tests/Visual/TestCaseIconButton.cs b/osu.Game.Tests/Visual/TestCaseIconButton.cs index 525e867c56..fae79e25bd 100644 --- a/osu.Game.Tests/Visual/TestCaseIconButton.cs +++ b/osu.Game.Tests/Visual/TestCaseIconButton.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics; @@ -12,6 +13,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseIconButton : OsuTestCase { public TestCaseIconButton() diff --git a/osu.Game.Tests/Visual/TestCaseIntroSequence.cs b/osu.Game.Tests/Visual/TestCaseIntroSequence.cs index 97116e7746..4af6255b48 100644 --- a/osu.Game.Tests/Visual/TestCaseIntroSequence.cs +++ b/osu.Game.Tests/Visual/TestCaseIntroSequence.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,6 +13,7 @@ using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseIntroSequence : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseKeyConfiguration.cs b/osu.Game.Tests/Visual/TestCaseKeyConfiguration.cs index 57bb36d144..e39b9f6683 100644 --- a/osu.Game.Tests/Visual/TestCaseKeyConfiguration.cs +++ b/osu.Game.Tests/Visual/TestCaseKeyConfiguration.cs @@ -1,10 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseKeyConfiguration : OsuTestCase { private readonly KeyBindingOverlay overlay; diff --git a/osu.Game.Tests/Visual/TestCaseKeyCounter.cs b/osu.Game.Tests/Visual/TestCaseKeyCounter.cs index ff1b320b5a..bf73c6899b 100644 --- a/osu.Game.Tests/Visual/TestCaseKeyCounter.cs +++ b/osu.Game.Tests/Visual/TestCaseKeyCounter.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.MathUtils; using osu.Game.Screens.Play; @@ -8,6 +9,7 @@ using OpenTK.Input; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseKeyCounter : OsuTestCase { public TestCaseKeyCounter() diff --git a/osu.Game.Tests/Visual/TestCaseMedalOverlay.cs b/osu.Game.Tests/Visual/TestCaseMedalOverlay.cs index f11c37f5b2..8d91a0f0dd 100644 --- a/osu.Game.Tests/Visual/TestCaseMedalOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseMedalOverlay.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Game.Overlays; using osu.Game.Overlays.MedalSplash; using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseMedalOverlay : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseMusicController.cs b/osu.Game.Tests/Visual/TestCaseMusicController.cs index 9424a3fee7..2ddc57d7b4 100644 --- a/osu.Game.Tests/Visual/TestCaseMusicController.cs +++ b/osu.Game.Tests/Visual/TestCaseMusicController.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; @@ -11,6 +12,7 @@ using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseMusicController : OsuTestCase { private readonly Bindable beatmapBacking = new Bindable(); diff --git a/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs b/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs index b2d3ac8c4d..2ba57f2bd2 100644 --- a/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -13,6 +14,7 @@ using osu.Game.Overlays.Notifications; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseNotificationOverlay : OsuTestCase { private readonly NotificationOverlay manager; diff --git a/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs b/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs index 9c6c50858f..6fe8bc5a8a 100644 --- a/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs +++ b/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs @@ -1,12 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseOnScreenDisplay : OsuTestCase { private FrameworkConfigManager config; diff --git a/osu.Game.Tests/Visual/TestCaseOsuGame.cs b/osu.Game.Tests/Visual/TestCaseOsuGame.cs index 9e6776800e..a802db6a10 100644 --- a/osu.Game.Tests/Visual/TestCaseOsuGame.cs +++ b/osu.Game.Tests/Visual/TestCaseOsuGame.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; @@ -12,6 +13,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseOsuGame : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 13b2be9fdb..cede0160bc 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.MathUtils; @@ -19,6 +20,7 @@ using osu.Game.Tests.Platform; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCasePlaySongSelect : OsuTestCase { private BeatmapManager manager; diff --git a/osu.Game.Tests/Visual/TestCasePlaybackControl.cs b/osu.Game.Tests/Visual/TestCasePlaybackControl.cs index 82c0b8f4fd..43e977ba23 100644 --- a/osu.Game.Tests/Visual/TestCasePlaybackControl.cs +++ b/osu.Game.Tests/Visual/TestCasePlaybackControl.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Screens.Edit.Components; @@ -9,6 +10,7 @@ using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCasePlaybackControl : OsuTestCase { public TestCasePlaybackControl() diff --git a/osu.Game.Tests/Visual/TestCasePopupDialog.cs b/osu.Game.Tests/Visual/TestCasePopupDialog.cs index e3bae3955a..8d830672b7 100644 --- a/osu.Game.Tests/Visual/TestCasePopupDialog.cs +++ b/osu.Game.Tests/Visual/TestCasePopupDialog.cs @@ -1,12 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Overlays.Dialog; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCasePopupDialog : OsuTestCase { public TestCasePopupDialog() diff --git a/osu.Game.Tests/Visual/TestCaseRankGraph.cs b/osu.Game.Tests/Visual/TestCaseRankGraph.cs index 88631aa982..ad53238e76 100644 --- a/osu.Game.Tests/Visual/TestCaseRankGraph.cs +++ b/osu.Game.Tests/Visual/TestCaseRankGraph.cs @@ -9,11 +9,13 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using System.Collections.Generic; using System; +using NUnit.Framework; using osu.Game.Graphics.UserInterface; using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseRankGraph : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs index 237687458d..115ac11919 100644 --- a/osu.Game.Tests/Visual/TestCaseReplay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplay.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual // We create a dummy RulesetContainer just to get the replay - we don't want to use mods here // to simulate setting a replay rather than having the replay already set for us beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); - var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap, false); + var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap, beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo)); // We have the replay var replay = dummyRulesetContainer.Replay; diff --git a/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs b/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs index 595a93b194..a1b683b64c 100644 --- a/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.HUD; @@ -8,6 +9,7 @@ using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseReplaySettingsOverlay : OsuTestCase { public TestCaseReplaySettingsOverlay() diff --git a/osu.Game.Tests/Visual/TestCaseResults.cs b/osu.Game.Tests/Visual/TestCaseResults.cs index 012d31e75a..06bdfdb7e1 100644 --- a/osu.Game.Tests/Visual/TestCaseResults.cs +++ b/osu.Game.Tests/Visual/TestCaseResults.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; @@ -11,6 +12,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseResults : OsuTestCase { private BeatmapManager beatmaps; diff --git a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs index 8c4aa02a68..c45312392f 100644 --- a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs +++ b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -11,6 +12,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseRoomInspector : OsuTestCase { private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/TestCaseScoreCounter.cs b/osu.Game.Tests/Visual/TestCaseScoreCounter.cs index a8dc96ad72..e657035355 100644 --- a/osu.Game.Tests/Visual/TestCaseScoreCounter.cs +++ b/osu.Game.Tests/Visual/TestCaseScoreCounter.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.MathUtils; @@ -10,6 +11,7 @@ using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseScoreCounter : OsuTestCase { public TestCaseScoreCounter() diff --git a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs index 21d967c3e3..cfa4846939 100644 --- a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using OpenTK; using osu.Framework.Graphics; @@ -16,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseScrollingHitObjects : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(Playfield) }; diff --git a/osu.Game.Tests/Visual/TestCaseSettings.cs b/osu.Game.Tests/Visual/TestCaseSettings.cs index 923ae540db..3f42f2e863 100644 --- a/osu.Game.Tests/Visual/TestCaseSettings.cs +++ b/osu.Game.Tests/Visual/TestCaseSettings.cs @@ -1,12 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseSettings : OsuTestCase { private readonly SettingsOverlay settings; diff --git a/osu.Game.Tests/Visual/TestCaseSkipButton.cs b/osu.Game.Tests/Visual/TestCaseSkipButton.cs index 3fd66f8be3..a4d2019cd7 100644 --- a/osu.Game.Tests/Visual/TestCaseSkipButton.cs +++ b/osu.Game.Tests/Visual/TestCaseSkipButton.cs @@ -1,10 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseSkipButton : OsuTestCase { protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/TestCaseSocial.cs b/osu.Game.Tests/Visual/TestCaseSocial.cs index d3ff18b37f..4003d834d5 100644 --- a/osu.Game.Tests/Visual/TestCaseSocial.cs +++ b/osu.Game.Tests/Visual/TestCaseSocial.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Game.Overlays; using osu.Game.Overlays.Social; using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseSocial : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseSongProgress.cs b/osu.Game.Tests/Visual/TestCaseSongProgress.cs index 2320e8d8db..857fd6c902 100644 --- a/osu.Game.Tests/Visual/TestCaseSongProgress.cs +++ b/osu.Game.Tests/Visual/TestCaseSongProgress.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.MathUtils; using osu.Framework.Timing; @@ -10,6 +11,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseSongProgress : OsuTestCase { private readonly SongProgress progress; diff --git a/osu.Game.Tests/Visual/TestCaseStoryboard.cs b/osu.Game.Tests/Visual/TestCaseStoryboard.cs index 089733c57e..d34a0e0e5f 100644 --- a/osu.Game.Tests/Visual/TestCaseStoryboard.cs +++ b/osu.Game.Tests/Visual/TestCaseStoryboard.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; @@ -14,6 +15,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseStoryboard : OsuTestCase { private readonly Bindable beatmapBacking = new Bindable(); diff --git a/osu.Game.Tests/Visual/TestCaseTextAwesome.cs b/osu.Game.Tests/Visual/TestCaseTextAwesome.cs index 830dea406a..bf7609ff8d 100644 --- a/osu.Game.Tests/Visual/TestCaseTextAwesome.cs +++ b/osu.Game.Tests/Visual/TestCaseTextAwesome.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -10,6 +11,7 @@ using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseTextAwesome : OsuTestCase { public TestCaseTextAwesome() diff --git a/osu.Game.Tests/Visual/TestCaseToolbar.cs b/osu.Game.Tests/Visual/TestCaseToolbar.cs index b596c4d5e0..94e45fe0c2 100644 --- a/osu.Game.Tests/Visual/TestCaseToolbar.cs +++ b/osu.Game.Tests/Visual/TestCaseToolbar.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Toolbar; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseToolbar : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseUserPanel.cs b/osu.Game.Tests/Visual/TestCaseUserPanel.cs index b18edf0ccb..ed377dc160 100644 --- a/osu.Game.Tests/Visual/TestCaseUserPanel.cs +++ b/osu.Game.Tests/Visual/TestCaseUserPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Users; @@ -8,6 +9,7 @@ using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseUserPanel : OsuTestCase { public TestCaseUserPanel() diff --git a/osu.Game.Tests/Visual/TestCaseUserProfile.cs b/osu.Game.Tests/Visual/TestCaseUserProfile.cs index 8acc8d1b5b..1fc6c6f224 100644 --- a/osu.Game.Tests/Visual/TestCaseUserProfile.cs +++ b/osu.Game.Tests/Visual/TestCaseUserProfile.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -11,6 +12,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseUserProfile : OsuTestCase { private readonly TestUserProfileOverlay profile; @@ -56,6 +58,12 @@ namespace osu.Game.Tests.Visual checkSupporterTag(false); + AddStep("Show null dummy", () => profile.ShowUser(new User + { + Username = @"Null", + Id = 1, + }, false)); + AddStep("Show ppy", () => profile.ShowUser(new User { Username = @"peppy", diff --git a/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs b/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs new file mode 100644 index 0000000000..1f7a7e7165 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs @@ -0,0 +1,161 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Profile.Sections; +using osu.Game.Overlays.Profile.Sections.Recent; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseUserProfileRecentSection : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(RecentSection), + typeof(DrawableRecentActivity), + typeof(PaginatedRecentActivityContainer), + typeof(MedalIcon) + }; + + public TestCaseUserProfileRecentSection() + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.2f) + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + ChildrenEnumerable = createDummyActivities().Select(a => new DrawableRecentActivity(a)) + }, + } + }; + } + + private IEnumerable createDummyActivities() + { + var dummyBeatmap = new RecentActivity.RecentActivityBeatmap + { + Title = @"Dummy beatmap", + Url = "/b/1337", + }; + + var dummyUser = new RecentActivity.RecentActivityUser + { + Username = "DummyReborn", + Url = "/u/666", + PreviousUsername = "Dummy", + }; + + return new[] + { + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.Achievement, + Achievement = new RecentActivity.RecentActivityAchievement + { + Name = @"Feelin' It", + Slug = @"all-secret-feelinit", + }, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapPlaycount, + Count = 1337, + Beatmap = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetApprove, + Approval = BeatmapApproval.Qualified, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetDelete, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetRevive, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetRevive, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetUpdate, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetUpload, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.Rank, + Rank = 1, + Mode = "osu!", + Beatmap = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.RankLost, + Mode = "osu!", + Beatmap = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UsernameChange, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UserSupportAgain, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UserSupportFirst, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UserSupportGift, + }, + }; + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseUserRanks.cs b/osu.Game.Tests/Visual/TestCaseUserRanks.cs index 1926585f07..effc98c381 100644 --- a/osu.Game.Tests/Visual/TestCaseUserRanks.cs +++ b/osu.Game.Tests/Visual/TestCaseUserRanks.cs @@ -10,9 +10,11 @@ using osu.Game.Overlays.Profile.Sections.Ranks; using osu.Game.Users; using System; using System.Collections.Generic; +using NUnit.Framework; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseUserRanks : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(DrawableProfileScore), typeof(RanksSection) }; diff --git a/osu.Game.Tests/Visual/TestCaseVolumePieces.cs b/osu.Game.Tests/Visual/TestCaseVolumePieces.cs new file mode 100644 index 0000000000..cfbf7fdb4d --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseVolumePieces.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Overlays.Volume; +using OpenTK.Graphics; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseVolumePieces : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(VolumeMeter), typeof(MuteButton) }; + + protected override void LoadComplete() + { + VolumeMeter meter; + MuteButton mute; + Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue)); + Add(mute = new MuteButton + { + Margin = new MarginPadding { Top = 200 } + }); + + AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1); + AddToggleStep("mute", b => mute.Current.Value = b); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseWaveform.cs b/osu.Game.Tests/Visual/TestCaseWaveform.cs index 87492e2332..7d4a9d663b 100644 --- a/osu.Game.Tests/Visual/TestCaseWaveform.cs +++ b/osu.Game.Tests/Visual/TestCaseWaveform.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; @@ -15,6 +16,7 @@ using osu.Game.Screens.Edit.Screens.Compose.Timeline; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseWaveform : OsuTestCase { private readonly Bindable beatmapBacking = new Bindable(); diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 14810abf84..1cfa7bc111 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -173,7 +173,9 @@ + + diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index e6f4a0b8d1..2014db6c61 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Audio.Sample; namespace osu.Game.Audio { @@ -14,22 +13,10 @@ namespace osu.Game.Audio public const string HIT_NORMAL = @"hitnormal"; public const string HIT_CLAP = @"hitclap"; - public SampleChannel GetChannel(SampleManager manager, string resourceNamespace = null) - { - SampleChannel channel = null; - - if (resourceNamespace != null) - channel = manager.Get($"Gameplay/{resourceNamespace}/{Bank}-{Name}"); - - // try without namespace as a fallback. - if (channel == null) - channel = manager.Get($"Gameplay/{Bank}-{Name}"); - - if (channel != null) - channel.Volume.Value = Volume / 100.0; - - return channel; - } + /// + /// An optional ruleset namespace. + /// + public string Namespace; /// /// The bank to load the sample from. diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 20de4e9680..711e220b88 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -12,8 +12,16 @@ namespace osu.Game.Beatmaps /// Converts a Beatmap for another mode. /// /// The type of HitObject stored in the Beatmap. - public abstract class BeatmapConverter where T : HitObject + public abstract class BeatmapConverter : IBeatmapConverter + where T : HitObject { + private event Action> ObjectConverted; + event Action> IBeatmapConverter.ObjectConverted + { + add => ObjectConverted += value; + remove => ObjectConverted -= value; + } + /// /// Checks if a Beatmap can be converted using this Beatmap Converter. /// @@ -32,6 +40,8 @@ namespace osu.Game.Beatmaps return ConvertBeatmap(new Beatmap(original)); } + void IBeatmapConverter.Convert(Beatmap original) => Convert(original); + /// /// Performs the conversion of a Beatmap using this Beatmap Converter. /// @@ -63,8 +73,11 @@ namespace osu.Game.Beatmaps yield break; } + var converted = ConvertHitObject(original, beatmap).ToList(); + ObjectConverted?.Invoke(original, converted); + // Convert the hit object - foreach (var obj in ConvertHitObject(original, beatmap)) + foreach (var obj in converted) { if (obj == null) continue; diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 16e6692887..38b84b4b03 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -29,8 +29,8 @@ namespace osu.Game.Beatmaps set => approachRate = value; } - public float SliderMultiplier { get; set; } = 1; - public float SliderTickRate { get; set; } = 1; + public double SliderMultiplier { get; set; } = 1; + public double SliderTickRate { get; set; } = 1; /// /// Maps a difficulty value [0, 10] to a two-piece linear range of values. diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 69027ffd73..2b42553891 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using OpenTK; + namespace osu.Game.Beatmaps.ControlPoints { public class DifficultyControlPoint : ControlPoint @@ -8,6 +10,12 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The speed multiplier at this control point. /// - public double SpeedMultiplier = 1; + public double SpeedMultiplier + { + get => speedMultiplier; + set => speedMultiplier = MathHelper.Clamp(value, 0.1, 10); + } + + private double speedMultiplier = 1; } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 0592ef38c5..0db1f08a90 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using OpenTK; using osu.Game.Beatmaps.Timing; namespace osu.Game.Beatmaps.ControlPoints @@ -15,6 +16,12 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The beat length at this control point. /// - public double BeatLength = 1000; + public double BeatLength + { + get => beatLength; + set => beatLength = MathHelper.Clamp(value, 6, 60000); + } + + private double beatLength = 1000; } } diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs index d61c62a30b..2bea31c0d3 100644 --- a/osu.Game/Beatmaps/DifficultyCalculator.cs +++ b/osu.Game/Beatmaps/DifficultyCalculator.cs @@ -47,12 +47,12 @@ namespace osu.Game.Beatmaps foreach (var mod in Mods.OfType()) mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty); + foreach (var h in Beatmap.HitObjects) + h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty); + foreach (var mod in mods.OfType>()) foreach (var obj in Beatmap.HitObjects) mod.ApplyToHitObject(obj); - - foreach (var h in Beatmap.HitObjects) - h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty); } protected virtual void PreprocessHitObjects() diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 3847787a4c..7d4f8b5bf5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -8,6 +8,7 @@ using OpenTK.Graphics; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Beatmaps.ControlPoints; +using osu.Framework; namespace osu.Game.Beatmaps.Formats { @@ -21,6 +22,19 @@ namespace osu.Game.Beatmaps.Formats private LegacySampleBank defaultSampleBank; private int defaultSampleVolume = 100; + /// + /// lazer's audio timings in general doesn't match stable. this is the result of user testing, albeit limited. + /// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + /// + public static int UniversalOffset => RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? -22 : 0; + + /// + /// Whether or not beatmap or runtime offsets should be applied. Defaults on; only disable for testing purposes. + /// + public bool ApplyOffsets = true; + + private readonly int offset = UniversalOffset; + public LegacyBeatmapDecoder() { } @@ -28,6 +42,9 @@ namespace osu.Game.Beatmaps.Formats public LegacyBeatmapDecoder(string header) { BeatmapVersion = int.Parse(header.Substring(17)); + + // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) + offset += BeatmapVersion < 5 ? 24 : 0; } protected override void ParseBeatmap(StreamReader stream, Beatmap beatmap) @@ -42,6 +59,10 @@ namespace osu.Game.Beatmaps.Formats ParseContent(stream); + // objects may be out of order *only* if a user has manually edited an .osu file. + // unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828). + this.beatmap.HitObjects.Sort((x, y) => x.StartTime.CompareTo(y.StartTime)); + foreach (var hitObject in this.beatmap.HitObjects) hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty); } @@ -98,7 +119,7 @@ namespace osu.Game.Beatmaps.Formats beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value); break; case @"PreviewTime": - metadata.PreviewTime = int.Parse(pair.Value); + metadata.PreviewTime = getOffsetTime(int.Parse(pair.Value)); break; case @"Countdown": beatmap.BeatmapInfo.Countdown = int.Parse(pair.Value) == 1; @@ -228,10 +249,10 @@ namespace osu.Game.Beatmaps.Formats difficulty.ApproachRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); break; case @"SliderMultiplier": - difficulty.SliderMultiplier = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.SliderMultiplier = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); break; case @"SliderTickRate": - difficulty.SliderTickRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.SliderTickRate = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); break; } } @@ -253,8 +274,8 @@ namespace osu.Game.Beatmaps.Formats case EventType.Break: var breakEvent = new BreakPeriod { - StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo), - EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo) + StartTime = getOffsetTime(double.Parse(split[1], NumberFormatInfo.InvariantInfo)), + EndTime = getOffsetTime(double.Parse(split[2], NumberFormatInfo.InvariantInfo)) }; if (!breakEvent.HasEffect) @@ -269,7 +290,7 @@ namespace osu.Game.Beatmaps.Formats { string[] split = line.Split(','); - double time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo); + double time = getOffsetTime(double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo)); double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; @@ -392,7 +413,14 @@ namespace osu.Game.Beatmaps.Formats var obj = parser.Parse(line); if (obj != null) + { + obj.StartTime = getOffsetTime(obj.StartTime); beatmap.HitObjects.Add(obj); + } } + + private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0); + + private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0); } } diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs new file mode 100644 index 0000000000..ebd900b97e --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapConverter.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Beatmaps +{ + public interface IBeatmapConverter + { + /// + /// Invoked when a has been converted. + /// The first argument contains the that was converted. + /// The second argument contains the s that were output from the conversion process. + /// + event Action> ObjectConverted; + + /// + /// Converts a Beatmap using this Beatmap Converter. + /// + /// The un-converted Beatmap. + void Convert(Beatmap beatmap); + } +} diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 9f1b44af44..1d231ada23 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -90,6 +90,10 @@ namespace osu.Game.Graphics.Containers case LinkAction.External: Process.Start(url); break; + case LinkAction.OpenUserProfile: + if (long.TryParse(linkArgument, out long userId)) + game?.ShowUser(userId); + break; default: throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action."); } diff --git a/osu.Game/Graphics/Containers/ParallaxContainer.cs b/osu.Game/Graphics/Containers/ParallaxContainer.cs index cb894ca382..97d6225534 100644 --- a/osu.Game/Graphics/Containers/ParallaxContainer.cs +++ b/osu.Game/Graphics/Containers/ParallaxContainer.cs @@ -8,12 +8,15 @@ using OpenTK; using osu.Framework.Allocation; using osu.Game.Configuration; using osu.Framework.Configuration; +using osu.Framework.MathUtils; namespace osu.Game.Graphics.Containers { public class ParallaxContainer : Container, IRequireHighFrequencyMousePosition { - public float ParallaxAmount = 0.02f; + public const float DEFAULT_PARALLAX_AMOUNT = 0.02f; + + public float ParallaxAmount = DEFAULT_PARALLAX_AMOUNT; private Bindable parallaxEnabled; @@ -61,9 +64,12 @@ namespace osu.Game.Graphics.Containers if (parallaxEnabled) { - Vector2 offset = input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.NativeState.Position) - DrawSize / 2; - content.MoveTo(offset * ParallaxAmount, firstUpdate ? 0 : 1000, Easing.OutQuint); - content.Scale = new Vector2(1 + ParallaxAmount); + Vector2 offset = (input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.NativeState.Position) - DrawSize / 2) * ParallaxAmount; + + double elapsed = MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 1000); + + content.Position = Interpolation.ValueAt(elapsed, content.Position, offset, 0, 1000, Easing.OutQuint); + content.Scale = Interpolation.ValueAt(elapsed, content.Scale, new Vector2(1 + ParallaxAmount), 0, 1000, Easing.OutQuint); } firstUpdate = false; diff --git a/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs index 9f028490ef..5803c8a5db 100644 --- a/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs +++ b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; -using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,7 +9,5 @@ namespace osu.Game.Graphics.Containers public class ReverseChildIDFillFlowContainer : FillFlowContainer where T : Drawable { protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); - - protected override IEnumerable FlowingChildren => base.FlowingChildren.Reverse(); } } diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs new file mode 100644 index 0000000000..a912f989e0 --- /dev/null +++ b/osu.Game/Graphics/DrawableDate.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Threading; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics +{ + public class DrawableDate : OsuSpriteText, IHasTooltip + { + private readonly DateTimeOffset date; + private ScheduledDelegate updateTask; + + public DrawableDate(DateTimeOffset date) + { + AutoSizeAxes = Axes.Both; + Font = "Exo2.0-RegularItalic"; + + this.date = date.ToLocalTime(); + } + + [BackgroundDependencyLoader] + private void load() + { + updateTime(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Scheduler.Add(updateTimeWithReschedule); + } + + private void updateTimeWithReschedule() + { + updateTime(); + + var diffToNow = DateTimeOffset.Now.Subtract(date); + + double timeUntilNextUpdate = 1000; + if (diffToNow.TotalSeconds > 60) + { + timeUntilNextUpdate *= 60; + if (diffToNow.TotalMinutes > 60) + { + timeUntilNextUpdate *= 60; + + if (diffToNow.TotalHours > 24) + timeUntilNextUpdate *= 24; + } + } + + Scheduler.AddDelayed(updateTimeWithReschedule, timeUntilNextUpdate); + } + + public override bool HandleMouseInput => true; + + private void updateTime() => Text = date.Humanize(); + public string TooltipText => date.ToString(); + } +} diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 43a3f06236..6d9bf231c3 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -18,8 +18,6 @@ namespace osu.Game.Graphics.UserInterface public Action Exit; - public override bool HandleLeftRightArrows => false; - private bool focus; public bool HoldFocus { diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 7ad9bc73a8..20385a7dae 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; @@ -56,6 +57,14 @@ namespace osu.Game.Graphics.UserInterface } } + protected override TabFillFlowContainer CreateTabFlow() => new OsuTabFillFlowContainer + { + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.Both, + Depth = -1, + Masking = true + }; + public class OsuTabItem : TabItem, IHasAccentColour { protected readonly SpriteText Text; @@ -239,5 +248,10 @@ namespace osu.Game.Graphics.UserInterface } } } + + private class OsuTabFillFlowContainer : TabFillFlowContainer + { + protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); + } } } diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index 9398eb55f3..28d33bbacd 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -12,6 +12,8 @@ namespace osu.Game.Graphics.UserInterface { protected virtual bool AllowCommit => false; + public override bool HandleLeftRightArrows => false; + public SearchTextBox() { Height = 35; diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs deleted file mode 100644 index ef3702fdf3..0000000000 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Bindings; -using osu.Game.Input.Bindings; - -namespace osu.Game.Graphics.UserInterface.Volume -{ - public class VolumeMeter : Container, IKeyBindingHandler - { - private readonly Box meterFill; - public BindableDouble Bindable { get; } = new BindableDouble(); - - public VolumeMeter(string meterName) - { - Size = new Vector2(40, 180); - Children = new Drawable[] - { - new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f, 0.9f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - Colour = Color4.DarkGray, - RelativeSizeAxes = Axes.Both - }, - meterFill = new Box - { - Colour = Color4.White, - Scale = new Vector2(1, 0), - RelativeSizeAxes = Axes.Both, - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre - } - } - }, - new OsuSpriteText - { - Text = meterName, - Anchor = Anchor.BottomCentre, - Origin = Anchor.TopCentre - } - }; - - Bindable.ValueChanged += delegate { updateFill(); }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - updateFill(); - } - - public double Volume - { - get => Bindable.Value; - private set => Bindable.Value = value; - } - - public void Increase() - { - Volume += 0.05f; - } - - public void Decrease() - { - Volume -= 0.05f; - } - - private void updateFill() => meterFill.ScaleTo(new Vector2(1, (float)Volume), 300, Easing.OutQuint); - - public bool OnPressed(GlobalAction action) - { - if (!IsHovered) return false; - - switch (action) - { - case GlobalAction.DecreaseVolume: - Decrease(); - return true; - case GlobalAction.IncreaseVolume: - Increase(); - return true; - } - - return false; - } - - public bool OnReleased(GlobalAction action) => false; - } -} diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 90f3999ddd..1325179e0d 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -199,7 +199,7 @@ namespace osu.Game.Online.API } catch (WebException we) { - HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? HttpStatusCode.RequestTimeout; + HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout); switch (statusCode) { diff --git a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs new file mode 100644 index 0000000000..d1685b01f3 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs @@ -0,0 +1,130 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Rulesets.Scoring; +using Humanizer; +using System; +using System.Collections.Generic; + +namespace osu.Game.Online.API.Requests +{ + public class GetUserRecentActivitiesRequest : APIRequest> + { + private readonly long userId; + private readonly int offset; + + public GetUserRecentActivitiesRequest(long userId, int offset = 0) + { + this.userId = userId; + this.offset = offset; + } + + protected override string Target => $"users/{userId}/recent_activity?offset={offset}"; + } + + public class RecentActivity + { + [JsonProperty("id")] + public int ID; + + [JsonProperty("createdAt")] + public DateTimeOffset CreatedAt; + + [JsonProperty] + private string type + { + set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize()); + } + + public RecentActivityType Type; + + [JsonProperty] + private string scoreRank + { + set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value); + } + + public ScoreRank ScoreRank; + + [JsonProperty("rank")] + public int Rank; + + [JsonProperty("approval")] + public BeatmapApproval Approval; + + [JsonProperty("count")] + public int Count; + + [JsonProperty("mode")] + public string Mode; + + [JsonProperty("beatmap")] + public RecentActivityBeatmap Beatmap; + + [JsonProperty("beatmapset")] + public RecentActivityBeatmap Beatmapset; + + [JsonProperty("user")] + public RecentActivityUser User; + + [JsonProperty("achievement")] + public RecentActivityAchievement Achievement; + + public class RecentActivityBeatmap + { + [JsonProperty("title")] + public string Title; + + [JsonProperty("url")] + public string Url; + } + + public class RecentActivityUser + { + [JsonProperty("username")] + public string Username; + + [JsonProperty("url")] + public string Url; + + [JsonProperty("previousUsername")] + public string PreviousUsername; + } + + public class RecentActivityAchievement + { + [JsonProperty("slug")] + public string Slug; + + [JsonProperty("name")] + public string Name; + } + + } + + public enum RecentActivityType + { + Achievement, + BeatmapPlaycount, + BeatmapsetApprove, + BeatmapsetDelete, + BeatmapsetRevive, + BeatmapsetUpdate, + BeatmapsetUpload, + Medal, + Rank, + RankLost, + UserSupportAgain, + UserSupportFirst, + UserSupportGift, + UsernameChange, + } + + public enum BeatmapApproval + { + Ranked, + Approved, + Qualified, + } +} diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 906f42d50e..9966f78435 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -118,6 +118,8 @@ namespace osu.Game.Online.Chat case "beatmapsets": case "d": return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]); + case "u": + return new LinkDetails(LinkAction.OpenUserProfile, args[3]); } } @@ -146,6 +148,9 @@ namespace osu.Game.Online.Chat case "spectate": linkType = LinkAction.Spectate; break; + case "u": + linkType = LinkAction.OpenUserProfile; + break; default: linkType = LinkAction.External; break; @@ -205,6 +210,15 @@ namespace osu.Game.Online.Chat return inputMessage; } + public static MessageFormatterResult FormatText(string text) + { + var result = format(text); + + result.Links.Sort(); + + return result; + } + public class MessageFormatterResult { public List Links = new List(); @@ -239,6 +253,7 @@ namespace osu.Game.Online.Chat OpenEditorTimestamp, JoinMultiplayerMatch, Spectate, + OpenUserProfile, } public class Link : IComparable diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 95eb88c5c8..e656c7256e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; using osu.Framework.Logging; -using osu.Game.Graphics.UserInterface.Volume; using osu.Framework.Allocation; using osu.Game.Overlays.Toolbar; using osu.Game.Screens; @@ -20,6 +19,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Audio; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Platform; using osu.Framework.Threading; @@ -32,6 +32,7 @@ using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Skinning; using OpenTK.Graphics; +using osu.Game.Overlays.Volume; namespace osu.Game { @@ -74,7 +75,7 @@ namespace osu.Game private OsuScreen screenStack; - private VolumeControl volume; + private VolumeOverlay volume; private OnScreenDisplay onscreenDisplay; private Bindable configRuleset; @@ -154,6 +155,12 @@ namespace osu.Game /// The set to display. public void ShowBeatmapSet(int setId) => beatmapSetOverlay.ShowBeatmapSet(setId); + /// + /// Show a user's profile as an overlay. + /// + /// The user to display. + public void ShowUser(long userId) => userProfile.ShowUser(userId); + protected void LoadScore(Score s) { scoreLoad?.Cancel(); @@ -231,7 +238,7 @@ namespace osu.Game }, }, overlayContent.Add); - loadComponentSingleFile(volume = new VolumeControl(), Add); + loadComponentSingleFile(volume = new VolumeOverlay(), overlayContent.Add); loadComponentSingleFile(onscreenDisplay = new OnScreenDisplay(), Add); //overlay elements @@ -414,6 +421,7 @@ namespace osu.Game sensitivity.Disabled = true; frameworkConfig.Set(FrameworkSetting.ActiveInputHandlers, string.Empty); + frameworkConfig.GetBindable(FrameworkSetting.ConfineMouseMode).SetDefault(); return true; case GlobalAction.ToggleToolbar: Toolbar.ToggleVisibility(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 94ed696e49..f3c46269d5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . +// Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; @@ -106,7 +106,7 @@ namespace osu.Game runMigrations(); - dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host)); + dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio)); dependencies.Cache(API = new APIAccess { diff --git a/osu.Game/Overlays/Chat/ChatTabControl.cs b/osu.Game/Overlays/Chat/ChatTabControl.cs index f028590bb4..1d3dab249d 100644 --- a/osu.Game/Overlays/Chat/ChatTabControl.cs +++ b/osu.Game/Overlays/Chat/ChatTabControl.cs @@ -53,9 +53,9 @@ namespace osu.Game.Overlays.Chat protected override void AddTabItem(TabItem item, bool addToDropdown = true) { - if (selectorTab.Depth < float.MaxValue) + if (item != selectorTab && TabContainer.GetLayoutPosition(selectorTab) < float.MaxValue) // performTabSort might've made selectorTab's position wonky, fix it - TabContainer.ChangeChildDepth(selectorTab, float.MaxValue); + TabContainer.SetLayoutPosition(selectorTab, float.MaxValue); base.AddTabItem(item, addToDropdown); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d7268fb186..d0a507be98 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -65,6 +65,14 @@ namespace osu.Game.Overlays.Mods Ruleset.TriggerChange(); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + Ruleset.UnbindAll(); + SelectedMods.UnbindAll(); + } + private void selectedModsChanged(IEnumerable obj) { foreach (ModSection section in ModSectionsContainer.Children) diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 31b7d0f9aa..03ce7fd88f 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -101,11 +101,10 @@ namespace osu.Game.Overlays.Music public void AddBeatmapSet(BeatmapSetInfo beatmapSet) { - items.Add(new PlaylistItem(beatmapSet) - { - OnSelect = set => OnSelect?.Invoke(set), - Depth = items.Count - }); + var newItem = new PlaylistItem(beatmapSet) { OnSelect = set => OnSelect?.Invoke(set) }; + + items.Add(newItem); + items.SetLayoutPosition(newItem, items.Count); } public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) @@ -197,7 +196,7 @@ namespace osu.Game.Overlays.Music { var itemsPos = items.ToLocalSpace(nativeDragPosition); - int srcIndex = (int)draggedItem.Depth; + int srcIndex = (int)items.GetLayoutPosition(draggedItem); // Find the last item with position < mouse position. Note we can't directly use // the item positions as they are being transformed @@ -219,15 +218,15 @@ namespace osu.Game.Overlays.Music if (srcIndex < dstIndex) { for (int i = srcIndex + 1; i <= dstIndex; i++) - items.ChangeChildDepth(items[i], i - 1); + items.SetLayoutPosition(items[i], i - 1); } else { for (int i = dstIndex; i < srcIndex; i++) - items.ChangeChildDepth(items[i], i + 1); + items.SetLayoutPosition(items[i], i + 1); } - items.ChangeChildDepth(draggedItem, dstIndex); + items.SetLayoutPosition(draggedItem, dstIndex); } private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren @@ -243,9 +242,6 @@ namespace osu.Game.Overlays.Music } } - // Compare with reversed ChildID and Depth - protected override int Compare(Drawable x, Drawable y) => base.Compare(y, x); - public IEnumerable FilterableChildren => Children; public ItemSearchContainer() diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 2f46bb4a71..48ad507d88 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -129,7 +129,6 @@ namespace osu.Game.Overlays public void Post(Notification notification) => postScheduler.Add(() => { ++runningDepth; - notification.Depth = notification.DisplayOnTop ? runningDepth : -runningDepth; notification.Closed += notificationClosed; @@ -138,7 +137,9 @@ namespace osu.Game.Overlays hasCompletionTarget.CompletionTarget = Post; var ourType = notification.GetType(); - sections.Children.FirstOrDefault(s => s.AcceptTypes.Any(accept => accept.IsAssignableFrom(ourType)))?.Add(notification); + + var section = sections.Children.FirstOrDefault(s => s.AcceptTypes.Any(accept => accept.IsAssignableFrom(ourType))); + section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth); updateCounts(); }); diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 13a69fbe3a..533f5326e3 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -25,10 +25,13 @@ namespace osu.Game.Overlays.Notifications private FlowContainer notifications; public int DisplayedCount => notifications.Count(n => !n.WasClosed); - public int UnreadCount => notifications.Count(n => !n.WasClosed && !n.Read); - public void Add(Notification notification) => notifications.Add(notification); + public void Add(Notification notification, float position) + { + notifications.Add(notification); + notifications.SetLayoutPosition(notification, position); + } public IEnumerable AcceptTypes; diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index d085800f41..f4b363cd91 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -130,11 +130,7 @@ namespace osu.Game.Overlays.Profile } } }, - infoTextLeft = new OsuTextFlowContainer(t => - { - t.TextSize = 14; - t.Alpha = 0.8f; - }) + infoTextLeft = new OsuTextFlowContainer(t => t.TextSize = 14) { X = UserProfileOverlay.CONTENT_X_MARGIN, Y = cover_height + 20, @@ -318,11 +314,23 @@ namespace osu.Game.Overlays.Profile colourBar.Show(); } - void boldItalic(SpriteText t) + void boldItalic(SpriteText t) => t.Font = @"Exo2.0-BoldItalic"; + void lightText(SpriteText t) => t.Alpha = 0.8f; + + OsuSpriteText createScoreText(string text) => new OsuSpriteText { - t.Font = @"Exo2.0-BoldItalic"; - t.Alpha = 1; - } + TextSize = 14, + Text = text + }; + + OsuSpriteText createScoreNumberText(string text) => new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-Bold", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = text + }; if (user.Age != null) { @@ -331,7 +339,7 @@ namespace osu.Game.Overlays.Profile if (user.Country != null) { - infoTextLeft.AddText("from "); + infoTextLeft.AddText("from ", lightText); infoTextLeft.AddText(user.Country.FullName, boldItalic); countryFlag.Country = user.Country; } @@ -344,18 +352,18 @@ namespace osu.Game.Overlays.Profile } else { - infoTextLeft.AddText("Joined "); - infoTextLeft.AddText(user.JoinDate.LocalDateTime.ToShortDateString(), boldItalic); + infoTextLeft.AddText("Joined ", lightText); + infoTextLeft.AddText(new DrawableDate(user.JoinDate), boldItalic); } infoTextLeft.NewLine(); - infoTextLeft.AddText("Last seen "); - infoTextLeft.AddText(user.LastVisit.LocalDateTime.ToShortDateString(), boldItalic); + infoTextLeft.AddText("Last seen ", lightText); + infoTextLeft.AddText(new DrawableDate(user.LastVisit), boldItalic); infoTextLeft.NewParagraph(); if (user.PlayStyle?.Length > 0) { - infoTextLeft.AddText("Plays with "); + infoTextLeft.AddText("Plays with ", lightText); infoTextLeft.AddText(string.Join(", ", user.PlayStyle), boldItalic); } @@ -411,23 +419,6 @@ namespace osu.Game.Overlays.Profile } } - // These could be local functions when C# 7 enabled - - private OsuSpriteText createScoreText(string text) => new OsuSpriteText - { - TextSize = 14, - Text = text - }; - - private OsuSpriteText createScoreNumberText(string text) => new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-Bold", - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Text = text - }; - private void tryAddInfoRightLine(FontAwesome icon, string str, string url = null) { if (string.IsNullOrEmpty(str)) return; @@ -436,10 +427,12 @@ namespace osu.Game.Overlays.Profile if (url != null) { infoTextRight.AddLink(" " + str, url); - } else + } + else { infoTextRight.AddText(" " + str); } + infoTextRight.NewLine(); } diff --git a/osu.Game/Overlays/Profile/RankGraph.cs b/osu.Game/Overlays/Profile/RankGraph.cs index 429049c7bc..369bdee65f 100644 --- a/osu.Game/Overlays/Profile/RankGraph.cs +++ b/osu.Game/Overlays/Profile/RankGraph.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Profile { placeholder.FadeIn(fade_duration, Easing.Out); - if (user == null) + if (user?.Statistics?.Ranks.Global == null) { rankText.Text = string.Empty; performanceText.Text = string.Empty; @@ -105,7 +105,7 @@ namespace osu.Game.Overlays.Profile return; } - int[] userRanks = user.RankHistory?.Data ?? new[] { user.Statistics.Ranks.Global }; + int[] userRanks = user.RankHistory?.Data ?? new[] { user.Statistics.Ranks.Global.Value }; ranks = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); if (ranks.Length > 1) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 51b202844a..509356ae04 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -40,24 +40,21 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks [BackgroundDependencyLoader(true)] private void load(OsuColour colour) { - RightFlowContainer.Add(new OsuSpriteText + var text = new OsuSpriteText { Text = $"accuracy: {Score.Accuracy:P2}", Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Colour = colour.GrayA, TextSize = 11, - Font = "Exo2.0-RegularItalic", - Depth = -1, - }); + Font = "Exo2.0-RegularItalic" + }; + + RightFlowContainer.Add(text); + RightFlowContainer.SetLayoutPosition(text, 1); LeftFlowContainer.Add(new BeatmapMetadataContainer(Score.Beatmap)); - LeftFlowContainer.Add(new OsuSpriteText - { - Text = Score.Date.LocalDateTime.ToShortDateString(), - TextSize = 11, - Colour = OsuColour.Gray(0xAA), - }); + LeftFlowContainer.Add(new DrawableDate(Score.Date)); foreach (Mod mod in Score.Mods) modsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.5f) }); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs new file mode 100644 index 0000000000..e8be8d1e44 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -0,0 +1,162 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; +using osu.Game.Screens.Select.Leaderboards; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class DrawableRecentActivity : DrawableProfileRow + { + private APIAccess api; + + private readonly RecentActivity activity; + + private LinkFlowContainer content; + + public DrawableRecentActivity(RecentActivity activity) + { + this.activity = activity; + } + + [BackgroundDependencyLoader] + private void load(APIAccess api) + { + this.api = api; + + LeftFlowContainer.Padding = new MarginPadding { Left = 10, Right = 160 }; + + LeftFlowContainer.Add(content = new LinkFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }); + + RightFlowContainer.Add(new DrawableDate(activity.CreatedAt) + { + TextSize = 13, + Colour = OsuColour.Gray(0xAA), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }); + + var formatted = createMessage(); + + content.AddLinks(formatted.Text, formatted.Links); + } + + protected override Drawable CreateLeftVisual() + { + switch (activity.Type) + { + case RecentActivityType.Rank: + return new DrawableRank(activity.ScoreRank) + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + + case RecentActivityType.Achievement: + return new MedalIcon(activity.Achievement.Slug) + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + + default: + return new Container + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + } + } + + private string toAbsoluteUrl(string url) => $"{api.Endpoint}{url}"; + + private MessageFormatter.MessageFormatterResult createMessage() + { + string userLinkTemplate() => $"[{toAbsoluteUrl(activity.User?.Url)} {activity.User?.Username}]"; + string beatmapLinkTemplate() => $"[{toAbsoluteUrl(activity.Beatmap?.Url)} {activity.Beatmap?.Title}]"; + string beatmapsetLinkTemplate() => $"[{toAbsoluteUrl(activity.Beatmapset?.Url)} {activity.Beatmapset?.Title}]"; + + string message; + + switch (activity.Type) + { + case RecentActivityType.Achievement: + message = $"{userLinkTemplate()} unlocked the {activity.Achievement.Name} medal!"; + break; + + case RecentActivityType.BeatmapPlaycount: + message = $"{beatmapLinkTemplate()} has been played {activity.Count} times!"; + break; + + case RecentActivityType.BeatmapsetApprove: + message = $"{beatmapsetLinkTemplate()} has been {activity.Approval.ToString().ToLowerInvariant()}!"; + break; + + case RecentActivityType.BeatmapsetDelete: + message = $"{beatmapsetLinkTemplate()} has been deleted."; + break; + + case RecentActivityType.BeatmapsetRevive: + message = $"{beatmapsetLinkTemplate()} has been revived from eternal slumber by {userLinkTemplate()}."; + break; + + case RecentActivityType.BeatmapsetUpdate: + message = $"{userLinkTemplate()} has updated the beatmap {beatmapsetLinkTemplate()}!"; + break; + + case RecentActivityType.BeatmapsetUpload: + message = $"{userLinkTemplate()} has submitted a new beatmap {beatmapsetLinkTemplate()}!"; + break; + + case RecentActivityType.Medal: + // apparently this shouldn't exist look at achievement instead (https://github.com/ppy/osu-web/blob/master/resources/assets/coffee/react/profile-page/recent-activity.coffee#L111) + message = string.Empty; + break; + + case RecentActivityType.Rank: + message = $"{userLinkTemplate()} achieved rank #{activity.Rank} on {beatmapLinkTemplate()} ({activity.Mode}!)"; + break; + + case RecentActivityType.RankLost: + message = $"{userLinkTemplate()} has lost first place on {beatmapLinkTemplate()} ({activity.Mode}!)"; + break; + + case RecentActivityType.UserSupportAgain: + message = $"{userLinkTemplate()} has once again chosen to support osu! - thanks for your generosity!"; + break; + + case RecentActivityType.UserSupportFirst: + message = $"{userLinkTemplate()} has become an osu! supporter - thanks for your generosity!"; + break; + + case RecentActivityType.UserSupportGift: + message = $"{userLinkTemplate()} has received the gift of osu! supporter!"; + break; + + case RecentActivityType.UsernameChange: + message = $"{activity.User?.PreviousUsername} has changed their username to {userLinkTemplate()}!"; + break; + + default: + message = string.Empty; + break; + } + + return MessageFormatter.FormatText(message); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs new file mode 100644 index 0000000000..6ffbe7193f --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class MedalIcon : Container + { + private readonly string slug; + private readonly Sprite sprite; + + private string url => $@"https://s.ppy.sh/images/medals-client/{slug}@2x.png"; + + public MedalIcon(string slug) + { + this.slug = slug; + + Child = sprite = new Sprite + { + Height = 40, + Width = 40, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + sprite.Texture = textures.Get(url); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs new file mode 100644 index 0000000000..d479627cde --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests; +using osu.Game.Users; +using System.Linq; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class PaginatedRecentActivityContainer : PaginatedContainer + { + public PaginatedRecentActivityContainer(Bindable user, string header, string missing) + : base(user, header, missing) + { + ItemsPerPage = 5; + } + + protected override void ShowMore() + { + base.ShowMore(); + + var req = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); + + req.Success += activities => + { + ShowMoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0); + ShowMoreLoading.Hide(); + + if (!activities.Any() && VisiblePages == 1) + { + MissingText.Show(); + return; + } + + MissingText.Hide(); + + foreach (RecentActivity activity in activities) + { + ItemsContainer.Add(new DrawableRecentActivity(activity)); + } + }; + + Api.Queue(req); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/RecentSection.cs b/osu.Game/Overlays/Profile/Sections/RecentSection.cs index 78b139efe8..84a941aa1a 100644 --- a/osu.Game/Overlays/Profile/Sections/RecentSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RecentSection.cs @@ -1,12 +1,22 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Overlays.Profile.Sections.Recent; + namespace osu.Game.Overlays.Profile.Sections { public class RecentSection : ProfileSection { public override string Title => "Recent"; - public override string Identifier => "recent_activities"; + public override string Identifier => "recent_activity"; + + public RecentSection() + { + Children = new[] + { + new PaginatedRecentActivityContainer(User, null, @"This user hasn't done anything notable recently!"), + }; + } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 5afc415d83..cc290fe1bb 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -45,7 +45,8 @@ namespace osu.Game.Overlays.Settings if (text == null) { // construct lazily for cases where the label is not needed (may be provided by the Control). - Add(text = new OsuSpriteText { Depth = 1 }); + Add(text = new OsuSpriteText()); + FlowContent.SetLayoutPosition(text, -1); } text.Text = value; diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 59f940a19d..aed0a6d7c6 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -73,6 +73,14 @@ namespace osu.Game.Overlays FadeEdgeEffectTo(0, DISAPPEAR_DURATION, Easing.Out); } + public void ShowUser(long userId) + { + if (userId == Header.User.Id) + return; + + ShowUser(new User { Id = userId }); + } + public void ShowUser(User user, bool fetchOnline = true) { userReq?.Cancel(); @@ -82,7 +90,7 @@ namespace osu.Game.Overlays sections = new ProfileSection[] { //new AboutSection(), - //new RecentSection(), + new RecentSection(), new RanksSection(), //new MedalsSection(), new HistoricalSection(), @@ -161,15 +169,18 @@ namespace osu.Game.Overlays { Header.User = user; - foreach (string id in user.ProfileOrder) + if (user.ProfileOrder != null) { - var sec = sections.FirstOrDefault(s => s.Identifier == id); - if (sec != null) + foreach (string id in user.ProfileOrder) { - sec.User.Value = user; + var sec = sections.FirstOrDefault(s => s.Identifier == id); + if (sec != null) + { + sec.User.Value = user; - sectionsContainer.Add(sec); - tabs.AddItem(sec); + sectionsContainer.Add(sec); + tabs.AddItem(sec); + } } } } diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs new file mode 100644 index 0000000000..adfc9c610f --- /dev/null +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -0,0 +1,83 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Graphics; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Volume +{ + public class MuteButton : Container, IHasCurrentValue + { + public Bindable Current { get; } = new Bindable(); + + private Color4 hoveredColour, unhoveredColour; + private const float width = 100; + public const float HEIGHT = 35; + + public MuteButton() + { + Masking = true; + BorderThickness = 3; + CornerRadius = HEIGHT / 2; + Size = new Vector2(width, HEIGHT); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + hoveredColour = colours.YellowDark; + BorderColour = unhoveredColour = colours.Gray1.Opacity(0.9f); + + SpriteIcon icon; + AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Alpha = 0.9f, + }, + icon = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20), + } + }); + + Current.ValueChanged += newValue => + { + icon.Icon = newValue ? FontAwesome.fa_volume_off : FontAwesome.fa_volume_up; + icon.Margin = new MarginPadding { Left = newValue ? width / 2 - 15 : width / 2 - 10 }; //Magic numbers to line up both icons because they're different widths + }; + Current.TriggerChange(); + } + + protected override bool OnHover(InputState state) + { + this.TransformTo("BorderColour", hoveredColour, 500, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(InputState state) + { + this.TransformTo("BorderColour", unhoveredColour, 500, Easing.OutQuint); + } + + protected override bool OnClick(InputState state) + { + Current.Value = !Current.Value; + return true; + } + } +} diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs similarity index 90% rename from osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs rename to osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 2328533665..a5be7dc445 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -7,7 +7,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Game.Input.Bindings; -namespace osu.Game.Graphics.UserInterface.Volume +namespace osu.Game.Overlays.Volume { public class VolumeControlReceptor : Container, IKeyBindingHandler, IHandleGlobalInput { diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs new file mode 100644 index 0000000000..64b9e513c4 --- /dev/null +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -0,0 +1,186 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Globalization; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Volume +{ + public class VolumeMeter : Container, IKeyBindingHandler + { + private CircularProgress volumeCircle; + public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; + private readonly float circleSize; + private readonly Color4 meterColour; + private readonly string name; + + private OsuSpriteText text; + private BufferedContainer maxGlow; + + public VolumeMeter(string name, float circleSize, Color4 meterColour) + { + this.circleSize = circleSize; + this.meterColour = meterColour; + this.name = name; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Add(new Container + { + Size = new Vector2(120, 20), + CornerRadius = 10, + Masking = true, + Margin = new MarginPadding { Left = circleSize + 10 }, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Alpha = 0.9f, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = "Exo2.0-Bold", + Text = name + } + } + }); + + CircularProgress bgProgress; + + Add(new CircularContainer + { + Masking = true, + Size = new Vector2(circleSize), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Alpha = 0.9f, + }, + bgProgress = new CircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = 0.05f, + Rotation = 180, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.Gray2, + Size = new Vector2(0.8f) + }, + (volumeCircle = new CircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = 0.05f, + Rotation = 180, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.8f) + }).WithEffect(new GlowEffect + { + Colour = meterColour, + Strength = 2 + }), + maxGlow = (text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = "Venera", + TextSize = 0.16f * circleSize + }).WithEffect(new GlowEffect + { + Colour = Color4.Transparent, + PadExtent = true, + }) + } + }); + + Bindable.ValueChanged += newVolume => { this.TransformTo("DisplayVolume", newVolume, 400, Easing.OutQuint); }; + bgProgress.Current.Value = 0.75f; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Bindable.TriggerChange(); + } + + private double displayVolume; + + protected double DisplayVolume + { + get => displayVolume; + set + { + displayVolume = value; + + if (displayVolume > 0.99f) + { + text.Text = "MAX"; + maxGlow.EffectColour = meterColour.Opacity(2f); + } + else + { + maxGlow.EffectColour = Color4.Transparent; + text.Text = Math.Round(displayVolume * 100).ToString(CultureInfo.CurrentCulture); + } + + volumeCircle.Current.Value = displayVolume * 0.75f; + } + } + + public double Volume + { + get => Bindable; + private set => Bindable.Value = value; + } + + public void Increase() => Volume += 0.05f; + + public void Decrease() => Volume -= 0.05f; + + public bool OnPressed(GlobalAction action) + { + if (!IsHovered) return false; + + switch (action) + { + case GlobalAction.DecreaseVolume: + Decrease(); + return true; + case GlobalAction.IncreaseVolume: + Increase(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; + } +} diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs b/osu.Game/Overlays/VolumeOverlay.cs similarity index 57% rename from osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs rename to osu.Game/Overlays/VolumeOverlay.cs index ccf70af6ed..17a4b139b0 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -1,57 +1,84 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Threading; -using OpenTK; -using osu.Framework.Audio; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Game.Graphics; using osu.Game.Input.Bindings; +using osu.Game.Overlays.Volume; +using OpenTK; +using OpenTK.Graphics; -namespace osu.Game.Graphics.UserInterface.Volume +namespace osu.Game.Overlays { - public class VolumeControl : OverlayContainer + public class VolumeOverlay : OverlayContainer { - private readonly VolumeMeter volumeMeterMaster; - private readonly IconButton muteIcon; + private const float offset = 10; + + private VolumeMeter volumeMeterMaster; + private VolumeMeter volumeMeterEffect; + private VolumeMeter volumeMeterMusic; + private MuteButton muteButton; protected override bool BlockPassThroughMouse => false; - public VolumeControl() - { - AutoSizeAxes = Axes.Both; - Anchor = Anchor.BottomRight; - Origin = Anchor.BottomRight; + private readonly BindableDouble muteAdjustment = new BindableDouble(); - Children = new Drawable[] + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + AddRange(new Drawable[] { + new Box + { + RelativeSizeAxes = Axes.Y, + Width = 300, + Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.75f), Color4.Black.Opacity(0)) + }, new FillFlowContainer { + Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 10, Right = 10, Top = 30, Bottom = 30 }, - Spacing = new Vector2(15, 0), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Spacing = new Vector2(0, offset), + Margin = new MarginPadding { Left = offset }, Children = new Drawable[] { - new Container + volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker) { - Size = new Vector2(IconButton.BUTTON_SIZE), - Child = muteIcon = new IconButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = FontAwesome.fa_volume_up, - Action = () => Adjust(GlobalAction.ToggleMute), - } + Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } //to counter the mute button and re-center the volume meters }, - volumeMeterMaster = new VolumeMeter("Master"), - volumeMeterEffect = new VolumeMeter("Effects"), - volumeMeterMusic = new VolumeMeter("Music") + volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker), + volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker), + muteButton = new MuteButton + { + Margin = new MarginPadding { Top = 100 } + } } - } + }, + }); + + volumeMeterMaster.Bindable.BindTo(audio.Volume); + volumeMeterEffect.Bindable.BindTo(audio.VolumeSample); + volumeMeterMusic.Bindable.BindTo(audio.VolumeTrack); + + muteButton.Current.ValueChanged += mute => + { + if (mute) + audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment); + else + audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment); }; } @@ -62,7 +89,13 @@ namespace osu.Game.Graphics.UserInterface.Volume volumeMeterMaster.Bindable.ValueChanged += _ => settingChanged(); volumeMeterEffect.Bindable.ValueChanged += _ => settingChanged(); volumeMeterMusic.Bindable.ValueChanged += _ => settingChanged(); - muted.ValueChanged += _ => settingChanged(); + muteButton.Current.ValueChanged += _ => settingChanged(); + } + + private void settingChanged() + { + Show(); + schedulePopOut(); } public bool Adjust(GlobalAction action) @@ -83,50 +116,15 @@ namespace osu.Game.Graphics.UserInterface.Volume return true; case GlobalAction.ToggleMute: Show(); - muted.Toggle(); + muteButton.Current.Value = !muteButton.Current; return true; } return false; } - private void settingChanged() - { - Show(); - schedulePopOut(); - } - - private readonly BindableDouble muteAdjustment = new BindableDouble(); - - private readonly BindableBool muted = new BindableBool(); - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - volumeMeterMaster.Bindable.BindTo(audio.Volume); - volumeMeterEffect.Bindable.BindTo(audio.VolumeSample); - volumeMeterMusic.Bindable.BindTo(audio.VolumeTrack); - - muted.ValueChanged += mute => - { - if (mute) - { - audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment); - muteIcon.Icon = FontAwesome.fa_volume_off; - } - else - { - audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment); - muteIcon.Icon = FontAwesome.fa_volume_up; - } - }; - } - private ScheduledDelegate popOutDelegate; - private readonly VolumeMeter volumeMeterEffect; - private readonly VolumeMeter volumeMeterMusic; - protected override void PopIn() { ClearTransforms(); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 1246127257..e6a51cc39b 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -4,14 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; -using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit.Layers; using osu.Game.Rulesets.Edit.Layers.Selection; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.UI; @@ -50,13 +49,25 @@ namespace osu.Game.Rulesets.Edit return; } - ScalableContainer createLayerContainerWithContent(Drawable content) + HitObjectOverlayLayer hitObjectOverlayLayer = CreateHitObjectOverlayLayer(); + SelectionLayer selectionLayer = new SelectionLayer(rulesetContainer.Playfield); + + var layerBelowRuleset = new BorderLayer { - var container = CreateLayerContainer(); - container.Child = content; - layerContainers.Add(container); - return container; - } + RelativeSizeAxes = Axes.Both, + Child = CreateLayerContainer() + }; + + var layerAboveRuleset = CreateLayerContainer(); + layerAboveRuleset.Children = new Drawable[] + { + selectionLayer, // Below object overlays for input + hitObjectOverlayLayer, + selectionLayer.CreateProxy() // Proxy above object overlays for selections + }; + + layerContainers.Add(layerBelowRuleset); + layerContainers.Add(layerAboveRuleset); RadioButtonCollection toolboxCollection; InternalChild = new GridContainer @@ -82,17 +93,9 @@ namespace osu.Game.Rulesets.Edit RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - createLayerContainerWithContent(new Container - { - Name = "Border", - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderColour = Color4.White, - BorderThickness = 2, - Child = new Box { RelativeSizeAxes = Axes.Both, Alpha = 0, AlwaysPresent = true } - }), + layerBelowRuleset, rulesetContainer, - createLayerContainerWithContent(new SelectionLayer(rulesetContainer.Playfield)) + layerAboveRuleset } } }, @@ -103,6 +106,9 @@ namespace osu.Game.Rulesets.Edit } }; + selectionLayer.ObjectSelected += hitObjectOverlayLayer.AddOverlay; + selectionLayer.ObjectDeselected += hitObjectOverlayLayer.RemoveOverlay; + toolboxCollection.Items = new[] { new RadioButton("Select", () => setCompositionTool(null)) } .Concat( @@ -136,5 +142,10 @@ namespace osu.Game.Rulesets.Edit /// Creates a which provides a layer above or below the . /// protected virtual ScalableContainer CreateLayerContainer() => new ScalableContainer { RelativeSizeAxes = Axes.Both }; + + /// + /// Creates the which overlays selected s. + /// + protected virtual HitObjectOverlayLayer CreateHitObjectOverlayLayer() => new HitObjectOverlayLayer(); } } diff --git a/osu.Game/Rulesets/Edit/Layers/BorderLayer.cs b/osu.Game/Rulesets/Edit/Layers/BorderLayer.cs new file mode 100644 index 0000000000..54c30b8d89 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/BorderLayer.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Edit.Layers +{ + public class BorderLayer : Container + { + protected override Container Content => content; + private readonly Container content; + + public BorderLayer() + { + InternalChildren = new Drawable[] + { + new Container + { + Name = "Border", + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + }, + content = new Container { RelativeSizeAxes = Axes.Both } + }; + } + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectOverlay.cs b/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectOverlay.cs new file mode 100644 index 0000000000..543dd2cc54 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectOverlay.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + public class HitObjectOverlay : OverlayContainer + { + // ReSharper disable once NotAccessedField.Local + // This will be used later to handle drag movement, etc + private readonly DrawableHitObject hitObject; + + public HitObjectOverlay(DrawableHitObject hitObject) + { + this.hitObject = hitObject; + + State = Visibility.Visible; + } + + protected override void PopIn() => Alpha = 1; + protected override void PopOut() => Alpha = 0; + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectOverlayLayer.cs b/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectOverlayLayer.cs new file mode 100644 index 0000000000..0b6e63d1fe --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectOverlayLayer.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + public class HitObjectOverlayLayer : CompositeDrawable + { + private readonly Dictionary existingOverlays = new Dictionary(); + + public HitObjectOverlayLayer() + { + RelativeSizeAxes = Axes.Both; + } + + /// + /// Adds an overlay for a which adds movement support. + /// + /// The to create an overlay for. + public void AddOverlay(DrawableHitObject hitObject) + { + var overlay = CreateOverlayFor(hitObject); + if (overlay == null) + return; + + existingOverlays[hitObject] = overlay; + AddInternal(overlay); + } + + /// + /// Removes the overlay for a . + /// + /// The to remove the overlay for. + public void RemoveOverlay(DrawableHitObject hitObject) + { + if (!existingOverlays.TryGetValue(hitObject, out var existing)) + return; + + existing.Hide(); + existing.Expire(); + } + + /// + /// Creates a for a specific . + /// + /// The to create the overlay for. + protected virtual HitObjectOverlay CreateOverlayFor(DrawableHitObject hitObject) => null; + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs index bda613f617..3895d34d7f 100644 --- a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs +++ b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs @@ -1,8 +1,10 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -15,6 +17,16 @@ namespace osu.Game.Rulesets.Edit.Layers.Selection { public class SelectionLayer : CompositeDrawable { + /// + /// Invoked when a is selected. + /// + public event Action ObjectSelected; + + /// + /// Invoked when a is deselected. + /// + public event Action ObjectDeselected; + private readonly Playfield playfield; public SelectionLayer(Playfield playfield) @@ -27,11 +39,11 @@ namespace osu.Game.Rulesets.Edit.Layers.Selection private SelectionBox selectionBox; private CaptureBox captureBox; - private readonly List selectedHitObjects = new List(); + private readonly HashSet selectedHitObjects = new HashSet(); protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - clearSelection(); + DeselectAll(); return true; } @@ -74,24 +86,85 @@ namespace osu.Game.Rulesets.Edit.Layers.Selection return true; } + /// + /// Selects a . + /// + /// The to select. + public void Select(DrawableHitObject hitObject) + { + if (!select(hitObject)) + return; + + clearCapture(); + finishSelection(); + } + + /// + /// Selects a without performing capture updates. + /// + /// The to select. + /// Whether was selected. + private bool select(DrawableHitObject hitObject) + { + if (!selectedHitObjects.Add(hitObject)) + return false; + + ObjectSelected?.Invoke(hitObject); + return true; + } + + /// + /// Deselects a . + /// + /// The to deselect. + public void Deselect(DrawableHitObject hitObject) + { + if (!deselect(hitObject)) + return; + + clearCapture(); + finishSelection(); + } + + /// + /// Deselects a without performing capture updates. + /// + /// The to deselect. + /// Whether the was deselected. + private bool deselect(DrawableHitObject hitObject) + { + if (!selectedHitObjects.Remove(hitObject)) + return false; + + ObjectDeselected?.Invoke(hitObject); + return true; + } + /// /// Deselects all selected s. /// - private void clearSelection() + public void DeselectAll() { + selectedHitObjects.ForEach(h => ObjectDeselected?.Invoke(h)); selectedHitObjects.Clear(); - captureBox?.Hide(); - captureBox?.Expire(); + + clearCapture(); } /// /// Selects all hitobjects that are present within the area of a . /// /// The selection . + // Todo: If needed we can severely reduce allocations in this method private void selectQuad(Quad screenSpaceQuad) { - foreach (var obj in playfield.HitObjects.Objects.Where(h => h.IsAlive && h.IsPresent && screenSpaceQuad.Contains(h.SelectionPoint))) - selectedHitObjects.Add(obj); + var expectedSelection = playfield.HitObjects.Objects.Where(h => h.IsAlive && h.IsPresent && screenSpaceQuad.Contains(h.SelectionPoint)).ToList(); + + var toRemove = selectedHitObjects.Except(expectedSelection).ToList(); + foreach (var obj in toRemove) + deselect(obj); + + expectedSelection.ForEach(h => select(h)); } /// @@ -100,11 +173,17 @@ namespace osu.Game.Rulesets.Edit.Layers.Selection /// The to select at. private void selectPoint(Vector2 screenSpacePoint) { - var selected = playfield.HitObjects.Objects.Reverse().Where(h => h.IsAlive && h.IsPresent).FirstOrDefault(h => h.ReceiveMouseInputAt(screenSpacePoint)); - if (selected == null) + var target = playfield.HitObjects.Objects.Reverse().Where(h => h.IsAlive && h.IsPresent).FirstOrDefault(h => h.ReceiveMouseInputAt(screenSpacePoint)); + if (target == null) return; - selectedHitObjects.Add(selected); + select(target); + } + + private void clearCapture() + { + captureBox?.Hide(); + captureBox?.Expire(); } private void finishSelection() diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index c1bf55b214..a3811654f2 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Judgements @@ -20,15 +21,18 @@ namespace osu.Game.Rulesets.Judgements { protected readonly Judgement Judgement; + public readonly DrawableHitObject JudgedObject; + protected readonly SpriteText JudgementText; /// /// Creates a drawable which visualises a . /// /// The judgement to visualise. - public DrawableJudgement(Judgement judgement) + public DrawableJudgement(Judgement judgement, DrawableHitObject judgedObject) { Judgement = judgement; + JudgedObject = judgedObject; AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 2db02724ed..394b6fa9fd 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -3,20 +3,19 @@ using System; using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Game.Rulesets.Judgements; -using Container = osu.Framework.Graphics.Containers.Container; -using osu.Game.Rulesets.Objects.Types; -using OpenTK.Graphics; -using osu.Game.Audio; using System.Linq; -using osu.Game.Graphics; +using osu.Framework.Allocation; using osu.Framework.Configuration; -using OpenTK; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; +using osu.Game.Audio; +using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables { @@ -32,11 +31,13 @@ namespace osu.Game.Rulesets.Objects.Drawables // Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first protected virtual string SampleNamespace => null; - protected List Samples = new List(); + protected SkinnableSound Samples; + protected virtual IEnumerable GetSamples() => HitObject.Samples; - private List nestedHitObjects; - public IReadOnlyList NestedHitObjects => nestedHitObjects; + private readonly Lazy> nestedHitObjects = new Lazy>(); + public bool HasNestedHitObjects => nestedHitObjects.IsValueCreated; + public IReadOnlyList NestedHitObjects => nestedHitObjects.Value; public event Action OnJudgement; public event Action OnJudgementRemoved; @@ -52,12 +53,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Whether this and all of its nested s have been hit. /// - public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && (NestedHitObjects?.All(n => n.IsHit) ?? true); + public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && (!HasNestedHitObjects || NestedHitObjects.All(n => n.IsHit)); /// /// Whether this and all of its nested s have been judged. /// - public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && (NestedHitObjects?.All(h => h.AllJudged) ?? true); + public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && (!HasNestedHitObjects || NestedHitObjects.All(h => h.AllJudged)); /// /// Whether this can be judged. @@ -83,31 +84,22 @@ namespace osu.Game.Rulesets.Objects.Drawables } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { - var samples = GetSamples(); + var samples = GetSamples().ToArray(); + if (samples.Any()) { if (HitObject.SampleControlPoint == null) throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); - - foreach (SampleInfo s in samples) + AddInternal(Samples = new SkinnableSound(samples.Select(s => new SampleInfo { - SampleInfo localSampleInfo = new SampleInfo - { - Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank, - Name = s.Name, - Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume - }; - - SampleChannel channel = localSampleInfo.GetChannel(audio.Sample, SampleNamespace); - - if (channel == null) - continue; - - Samples.Add(channel); - } + Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank, + Name = s.Name, + Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume, + Namespace = SampleNamespace + }).ToArray())); } } @@ -139,7 +131,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Plays all the hitsounds for this . /// - public void PlaySamples() => Samples.ForEach(s => s?.Play()); + public void PlaySamples() => Samples?.Play(); protected override void Update() { @@ -169,14 +161,11 @@ namespace osu.Game.Rulesets.Objects.Drawables protected virtual void AddNested(DrawableHitObject h) { - if (nestedHitObjects == null) - nestedHitObjects = new List(); - h.OnJudgement += (d, j) => OnJudgement?.Invoke(d, j); h.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(d, j); h.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j); - nestedHitObjects.Add(h); + nestedHitObjects.Value.Add(h); } /// @@ -220,11 +209,9 @@ namespace osu.Game.Rulesets.Objects.Drawables if (AllJudged) return false; - if (NestedHitObjects != null) - { + if (HasNestedHitObjects) foreach (var d in NestedHitObjects) judgementOccurred |= d.UpdateJudgement(userTriggered); - } if (!ProvidesJudgement || judgementFinalized || judgementOccurred) return judgementOccurred; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 5fdc9a07e1..ce292ef223 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -9,6 +9,7 @@ using System.Globalization; using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; +using osu.Framework.MathUtils; namespace osu.Game.Rulesets.Objects.Legacy { @@ -41,9 +42,11 @@ namespace osu.Game.Rulesets.Objects.Legacy } else if ((type & ConvertHitObjectType.Slider) > 0) { + var pos = new Vector2(int.Parse(split[0]), int.Parse(split[1])); + CurveType curveType = CurveType.Catmull; double length = 0; - var points = new List { new Vector2(int.Parse(split[0]), int.Parse(split[1])) }; + var points = new List { Vector2.Zero }; string[] pointsplit = split[5].Split('|'); foreach (string t in pointsplit) @@ -69,9 +72,14 @@ namespace osu.Game.Rulesets.Objects.Legacy } string[] temp = t.Split(':'); - points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture))); + points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos); } + // osu-stable special-cased colinear perfect curves to a CurveType.Linear + bool isLinear(List p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y)); + if (points.Count == 3 && curveType == CurveType.PerfectCurve && isLinear(points)) + curveType = CurveType.Linear; + int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture); if (repeatCount > 9000) @@ -134,7 +142,7 @@ namespace osu.Game.Rulesets.Objects.Legacy for (int i = 0; i < nodes; i++) nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - result = CreateSlider(new Vector2(int.Parse(split[0]), int.Parse(split[1])), combo, points, length, curveType, repeatCount, nodeSamples); + result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples); } else if ((type & ConvertHitObjectType.Spinner) > 0) { diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index 19f9a93976..d2a0530dd9 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using OpenTK; using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu Position = position, NewCombo = newCombo, ControlPoints = controlPoints, - Distance = length, + Distance = Math.Max(0, length), CurveType = curveType, RepeatSamples = repeatSamples, RepeatCount = repeatCount diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs index c03bdb240e..251ad3e3cd 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Objects.Types /// The curve. /// [0, 1] where 0 is the start time of the and 1 is the end time of the . /// The position on the curve. - public static Vector2 PositionAt(this IHasCurve obj, double progress) + public static Vector2 CurvePositionAt(this IHasCurve obj, double progress) => obj.Curve.PositionAt(obj.ProgressAt(progress)); /// diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index c245407bbf..5ffd67423e 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Input; -using osu.Framework.MathUtils; using osu.Game.Input.Handlers; using OpenTK; using OpenTK.Input; @@ -17,14 +16,15 @@ namespace osu.Game.Rulesets.Replays /// 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 abstract class FramedReplayInputHandler : ReplayInputHandler + public abstract class FramedReplayInputHandler : ReplayInputHandler + where TFrame : ReplayFrame { private readonly Replay replay; protected List Frames => replay.Frames; - public ReplayFrame CurrentFrame => !hasFrames ? null : Frames[currentFrameIndex]; - public ReplayFrame NextFrame => !hasFrames ? null : Frames[nextFrameIndex]; + public TFrame CurrentFrame => !HasFrames ? null : (TFrame)Frames[currentFrameIndex]; + public TFrame NextFrame => !HasFrames ? null : (TFrame)Frames[nextFrameIndex]; private int currentFrameIndex; @@ -46,31 +46,14 @@ namespace osu.Game.Rulesets.Replays return true; } - public void SetPosition(Vector2 pos) - { - } - - protected Vector2? Position - { - get - { - if (!hasFrames) - return null; - - return Interpolation.ValueAt(currentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time); - } - } - public override List GetPendingStates() => new List(); public bool AtLastFrame => currentFrameIndex == Frames.Count - 1; public bool AtFirstFrame => currentFrameIndex == 0; - public Vector2 Size => new Vector2(512, 384); - private const double sixty_frame_time = 1000.0 / 60; - private double currentTime; + protected double CurrentTime { get; private set; } private int currentDirection; /// @@ -79,14 +62,16 @@ namespace osu.Game.Rulesets.Replays /// public bool FrameAccuratePlayback = true; - private bool hasFrames => Frames.Count > 0; + protected bool HasFrames => Frames.Count > 0; private bool inImportantSection => - FrameAccuratePlayback && + HasFrames && FrameAccuratePlayback && //a button is in a pressed state - ((currentDirection > 0 ? CurrentFrame : NextFrame)?.IsImportant ?? false) && + IsImportant(currentDirection > 0 ? CurrentFrame : NextFrame) && //the next frame is within an allowable time span - Math.Abs(currentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2; + Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2; + + protected virtual bool IsImportant(TFrame frame) => false; /// /// Update the current frame based on an incoming time value. @@ -97,10 +82,10 @@ namespace osu.Game.Rulesets.Replays /// 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); + currentDirection = time.CompareTo(CurrentTime); if (currentDirection == 0) currentDirection = 1; - if (hasFrames) + if (HasFrames) { // check if the next frame is in the "future" for the current playback direction if (currentDirection != time.CompareTo(NextFrame.Time)) @@ -114,12 +99,12 @@ namespace osu.Game.Rulesets.Replays // If going backwards, we need to execute once _before_ the frame time to reverse any judgements // that would occur as a result of this frame in forward playback if (currentDirection == -1) - return currentTime = CurrentFrame.Time - 1; - return currentTime = CurrentFrame.Time; + return CurrentTime = CurrentFrame.Time - 1; + return CurrentTime = CurrentFrame.Time; } } - return currentTime = time; + return CurrentTime = time; } protected class ReplayMouseState : MouseState diff --git a/osu.Game/Rulesets/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Rulesets/Replays/Legacy/LegacyReplayFrame.cs new file mode 100644 index 0000000000..945cb95e79 --- /dev/null +++ b/osu.Game/Rulesets/Replays/Legacy/LegacyReplayFrame.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; + +namespace osu.Game.Rulesets.Replays.Legacy +{ + public class LegacyReplayFrame : ReplayFrame + { + public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); + + public float? MouseX; + public float? MouseY; + + public bool MouseLeft => MouseLeft1 || MouseLeft2; + public bool MouseRight => MouseRight1 || MouseRight2; + + public bool MouseLeft1 => (ButtonState & ReplayButtonState.Left1) > 0; + public bool MouseRight1 => (ButtonState & ReplayButtonState.Right1) > 0; + public bool MouseLeft2 => (ButtonState & ReplayButtonState.Left2) > 0; + public bool MouseRight2 => (ButtonState & ReplayButtonState.Right2) > 0; + + public ReplayButtonState ButtonState; + + public LegacyReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState) + : base(time) + { + MouseX = mouseX; + MouseY = mouseY; + ButtonState = buttonState; + } + + 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/Rulesets/Replays/ReplayButtonState.cs b/osu.Game/Rulesets/Replays/Legacy/ReplayButtonState.cs similarity index 85% rename from osu.Game/Rulesets/Replays/ReplayButtonState.cs rename to osu.Game/Rulesets/Replays/Legacy/ReplayButtonState.cs index 4421a79af8..d0706411d2 100644 --- a/osu.Game/Rulesets/Replays/ReplayButtonState.cs +++ b/osu.Game/Rulesets/Replays/Legacy/ReplayButtonState.cs @@ -3,7 +3,7 @@ using System; -namespace osu.Game.Rulesets.Replays +namespace osu.Game.Rulesets.Replays.Legacy { [Flags] public enum ReplayButtonState diff --git a/osu.Game/Rulesets/Replays/Replay.cs b/osu.Game/Rulesets/Replays/Replay.cs index 27a77addba..a0ea2c5655 100644 --- a/osu.Game/Rulesets/Replays/Replay.cs +++ b/osu.Game/Rulesets/Replays/Replay.cs @@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Replays public class Replay { public User User; - public List Frames = new List(); } } diff --git a/osu.Game/Rulesets/Replays/ReplayFrame.cs b/osu.Game/Rulesets/Replays/ReplayFrame.cs index 4f8ed5163e..61a3646024 100644 --- a/osu.Game/Rulesets/Replays/ReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/ReplayFrame.cs @@ -1,70 +1,19 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; - namespace osu.Game.Rulesets.Replays { public class ReplayFrame { - public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); - - public virtual bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight); - - public float? MouseX; - public float? MouseY; - - public bool MouseLeft => MouseLeft1 || MouseLeft2; - public bool MouseRight => MouseRight1 || MouseRight2; - - public bool MouseLeft1 - { - get { return (ButtonState & ReplayButtonState.Left1) > 0; } - set { setButtonState(ReplayButtonState.Left1, value); } - } - public bool MouseRight1 - { - get { return (ButtonState & ReplayButtonState.Right1) > 0; } - set { setButtonState(ReplayButtonState.Right1, value); } - } - public bool MouseLeft2 - { - get { return (ButtonState & ReplayButtonState.Left2) > 0; } - set { setButtonState(ReplayButtonState.Left2, value); } - } - public bool MouseRight2 - { - get { return (ButtonState & ReplayButtonState.Right2) > 0; } - set { setButtonState(ReplayButtonState.Right2, value); } - } - - private void setButtonState(ReplayButtonState singleButton, bool pressed) - { - if (pressed) - ButtonState |= singleButton; - else - ButtonState &= ~singleButton; - } - public double Time; - public ReplayButtonState ButtonState; - - protected ReplayFrame() + public ReplayFrame() { } - public ReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState) + public ReplayFrame(double time) { - MouseX = mouseX; - MouseY = mouseY; - ButtonState = buttonState; Time = 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/Rulesets/Replays/Types/IConvertibleReplayFrame.cs b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs new file mode 100644 index 0000000000..ac1e5e29ec --- /dev/null +++ b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Replays.Legacy; + +namespace osu.Game.Rulesets.Replays.Types +{ + /// + /// A type of which can be converted from a . + /// + public interface IConvertibleReplayFrame + { + /// + /// Populates this using values from a . + /// + /// The to extract values from. + /// The score. + /// The beatmap. + void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap); + } +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 4f256621fb..cba849a491 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -63,7 +64,7 @@ namespace osu.Game.Rulesets /// /// Do not override this unless you are a legacy mode. /// - public virtual int LegacyID => -1; + public virtual int? LegacyID => null; /// /// A unique short name to reference this ruleset in online requests. @@ -89,6 +90,13 @@ namespace osu.Game.Rulesets /// A descriptive name of the variant. public virtual string GetVariantName(int variant) => string.Empty; + /// + /// For rulesets which support legacy (osu-stable) replay conversion, this method will create an empty replay frame + /// for conversion use. + /// + /// An empty frame for the current ruleset, or null if unsupported. + public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; + /// /// Create a ruleset info based on this ruleset. /// diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 92fbf25f04..e621c3cf2b 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList(); //add all legacy modes in correct order - foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID)) + foreach (var r in instances.Where(r => r.LegacyID != null).OrderBy(r => r.LegacyID)) { if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null) context.RulesetInfo.Add(r.RulesetInfo); @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets context.SaveChanges(); //add any other modes - foreach (var r in instances.Where(r => r.LegacyID < 0)) + foreach (var r in instances.Where(r => r.LegacyID == null)) if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) context.RulesetInfo.Add(r.RulesetInfo); diff --git a/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs new file mode 100644 index 0000000000..9ebb62a368 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs @@ -0,0 +1,152 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.IO; +using osu.Game.Beatmaps; +using osu.Game.IO.Legacy; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Users; +using SharpCompress.Compressors.LZMA; + +namespace osu.Game.Rulesets.Scoring.Legacy +{ + public class LegacyScoreParser + { + private readonly RulesetStore rulesets; + private readonly BeatmapManager beatmaps; + + public LegacyScoreParser(RulesetStore rulesets, BeatmapManager beatmaps) + { + this.rulesets = rulesets; + this.beatmaps = beatmaps; + } + + private Beatmap currentBeatmap; + private Ruleset currentRuleset; + + public Score Parse(Stream stream) + { + Score score; + + using (SerializationReader sr = new SerializationReader(stream)) + { + score = new Score { Ruleset = rulesets.GetRuleset(sr.ReadByte()) }; + currentRuleset = score.Ruleset.CreateInstance(); + + /* score.Pass = true;*/ + var version = sr.ReadInt32(); + + /* score.FileChecksum = */ + var beatmapHash = sr.ReadString(); + score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash); + currentBeatmap = beatmaps.GetWorkingBeatmap(score.Beatmap).Beatmap; + + /* score.PlayerName = */ + score.User = new User { Username = 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 IOException("input .lzma is too short"); + long outSize = 0; + for (int i = 0; i < 8; i++) + { + int v = replayInStream.ReadByte(); + if (v < 0) + throw new IOException("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 Replay { User = score.User }; + readLegacyReplay(score.Replay, reader); + } + } + } + + return score; + } + + private void readLegacyReplay(Replay replay, StreamReader reader) + { + float lastTime = 0; + + foreach (var l in reader.ReadToEnd().Split(',')) + { + var split = l.Split('|'); + + if (split.Length < 4) + continue; + + if (split[0] == "-12345") + { + // Todo: The seed is provided in split[3], which we'll need to use at some point + continue; + } + + var diff = float.Parse(split[0]); + lastTime += diff; + + // Todo: At some point we probably want to rewind and play back the negative-time frames + // but for now we'll achieve equal playback to stable by skipping negative frames + if (diff < 0) + continue; + + replay.Frames.Add(convertFrame(new LegacyReplayFrame(lastTime, float.Parse(split[1]), float.Parse(split[2]), (ReplayButtonState)int.Parse(split[3])))); + } + } + + private ReplayFrame convertFrame(LegacyReplayFrame legacyFrame) + { + var convertible = currentRuleset.CreateConvertibleReplayFrame(); + if (convertible == null) + throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}"); + convertible.ConvertFrom(legacyFrame, currentBeatmap); + + var frame = (ReplayFrame)convertible; + frame.Time = legacyFrame.Time; + + return frame; + } + } +} diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index 7abee0b04f..cb2b76cdcf 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.cs @@ -2,16 +2,12 @@ // 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.Platform; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.IO.Legacy; using osu.Game.IPC; -using osu.Game.Rulesets.Replays; -using osu.Game.Users; -using SharpCompress.Compressors.LZMA; +using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets.Scoring { @@ -53,127 +49,8 @@ namespace osu.Game.Rulesets.Scoring public Score ReadReplayFile(string replayFilename) { - Score score; - using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename))) - using (SerializationReader sr = new SerializationReader(s)) - { - score = new Score - { - Ruleset = rulesets.GetRuleset(sr.ReadByte()) - }; - - /* score.Pass = true;*/ - var version = sr.ReadInt32(); - /* score.FileChecksum = */ - var beatmapHash = sr.ReadString(); - score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash); - /* score.PlayerName = */ - score.User = new User { Username = 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 IOException("input .lzma is too short"); - long outSize = 0; - for (int i = 0; i < 8; i++) - { - int v = replayInStream.ReadByte(); - if (v < 0) - throw new IOException("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 = createLegacyReplay(reader); - score.Replay.User = score.User; - } - } - } - - return score; + return new LegacyScoreParser(rulesets, beatmaps).Parse(s); } - - /// - /// Creates a legacy replay which is read from a stream. - /// - /// The stream reader. - /// The legacy replay. - private Replay createLegacyReplay(StreamReader reader) - { - var frames = new List(); - - float lastTime = 0; - - foreach (var l in reader.ReadToEnd().Split(',')) - { - var split = l.Split('|'); - - if (split.Length < 4) - continue; - - if (split[0] == "-12345") - { - // Todo: The seed is provided in split[3], which we'll need to use at some point - continue; - } - - var diff = float.Parse(split[0]); - lastTime += diff; - - // Todo: At some point we probably want to rewind and play back the negative-time frames - // but for now we'll achieve equal playback to stable by skipping negative frames - if (diff < 0) - continue; - - frames.Add(new ReplayFrame( - lastTime, - float.Parse(split[1]), - float.Parse(split[2]), - (ReplayButtonState)int.Parse(split[3]) - )); - } - - return new Replay { Frames = frames }; - } - } } diff --git a/osu.Game/Rulesets/UI/JudgementContainer.cs b/osu.Game/Rulesets/UI/JudgementContainer.cs new file mode 100644 index 0000000000..1291b9fc98 --- /dev/null +++ b/osu.Game/Rulesets/UI/JudgementContainer.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; + +namespace osu.Game.Rulesets.UI +{ + public class JudgementContainer : Container + where T : DrawableJudgement + { + public override void Add(T judgement) + { + if (judgement == null) throw new ArgumentNullException(nameof(judgement)); + + // remove any existing judgements for the judged object. + // this can be the case when rewinding. + RemoveAll(c => c.JudgedObject == judgement.JudgedObject); + + base.Add(judgement); + } + } +} diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 05cb0f741b..780bc5c86b 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -17,6 +17,7 @@ using osu.Framework.Configuration; using osu.Framework.Graphics.Cursor; using osu.Framework.Input; using osu.Game.Configuration; +using osu.Game.Input.Handlers; using osu.Game.Overlays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Replays; @@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.UI /// The input manager. public abstract PassThroughInputManager CreateInputManager(); - protected virtual FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => null; + protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null; public Replay Replay { get; private set; } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index f465d0e202..3f8a17e23d 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -91,8 +91,6 @@ namespace osu.Game.Rulesets.UI #region Clock control - protected override bool ShouldProcessClock => false; // We handle processing the clock ourselves - private ManualClock clock; private IFrameBasedClock parentClock; @@ -103,6 +101,7 @@ namespace osu.Game.Rulesets.UI //our clock will now be our parent's clock, but we want to replace this to allow manual control. parentClock = Clock; + ProcessCustomClock = false; Clock = new FramedClock(clock = new ManualClock { CurrentTime = parentClock.CurrentTime, diff --git a/osu.Game/Rulesets/UI/ScalableContainer.cs b/osu.Game/Rulesets/UI/ScalableContainer.cs index 43ed770f77..9762828e7d 100644 --- a/osu.Game/Rulesets/UI/ScalableContainer.cs +++ b/osu.Game/Rulesets/UI/ScalableContainer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.UI }); } - public class ScaledContainer : Container + private class ScaledContainer : Container { /// /// The value to scale the width of the content to match. diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs index 4cce90ee94..48c212efa7 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers var controlPoint = controlPointAt(obj.HitObject.StartTime); obj.LifetimeStart = obj.HitObject.StartTime - timeRange / controlPoint.Multiplier; - if (obj.NestedHitObjects != null) + if (obj.HasNestedHitObjects) { ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs index 94705426f8..1b7c3714d6 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers } } - if (obj.NestedHitObjects != null) + if (obj.HasNestedHitObjects) { ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); diff --git a/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs b/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs index 29ae35fca4..ae1e995373 100644 --- a/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs +++ b/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs @@ -4,8 +4,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using OpenTK; -using System.Collections.Generic; -using System.Linq; namespace osu.Game.Screens.Menu { @@ -22,8 +20,6 @@ namespace osu.Game.Screens.Menu protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); - protected override IEnumerable FlowingChildren => base.FlowingChildren.Reverse(); - public override Anchor Origin => Anchor.Custom; public override Vector2 OriginPosition diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index b91ff0d74b..3fcb885655 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Menu Alpha = 0.5f, Size = new Vector2(0.96f) }, - new BufferedContainer + new Container { AutoSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index a2d41dc206..8f4e08d4a2 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -52,6 +52,10 @@ namespace osu.Game.Screens protected readonly Bindable Beatmap = new Bindable(); + protected virtual float BackgroundParallaxAmount => 1; + + private ParallaxContainer backgroundParallaxContainer; + public WorkingBeatmap InitialBeatmap { set @@ -102,11 +106,10 @@ namespace osu.Game.Screens protected override void OnResuming(Screen last) { - base.OnResuming(last); - logo.AppendAnimatingAction(() => LogoArriving(logo, true), true); sampleExit?.Play(); + applyArrivingDefaults(true); - ShowOverlays.Value = ShowOverlaysOnEnter; + base.OnResuming(last); } protected override void OnSuspending(Screen next) @@ -123,6 +126,8 @@ namespace osu.Game.Screens if (lastOsu?.Background != null) { + backgroundParallaxContainer = lastOsu.backgroundParallaxContainer; + if (bg == null || lastOsu.Background.Equals(bg)) //we can keep the previous mode's background. Background = lastOsu.Background; @@ -136,7 +141,7 @@ namespace osu.Game.Screens // this makes up for the fact our padding changes when the global toolbar is visible. bg.Scale = new Vector2(1.06f); - AddInternal(new ParallaxContainer + AddInternal(backgroundParallaxContainer = new ParallaxContainer { Depth = float.MaxValue, Children = new[] @@ -149,11 +154,9 @@ namespace osu.Game.Screens if ((logo = lastOsu?.logo) == null) LoadComponentAsync(logo = new OsuLogo { Alpha = 0 }, AddInternal); - logo.AppendAnimatingAction(() => LogoArriving(logo, false), true); + applyArrivingDefaults(false); base.OnEntering(last); - - ShowOverlays.Value = ShowOverlaysOnEnter; } protected override bool OnExiting(Screen next) @@ -193,6 +196,16 @@ namespace osu.Game.Screens logo.Ripple = true; } + private void applyArrivingDefaults(bool isResuming) + { + logo.AppendAnimatingAction(() => LogoArriving(logo, isResuming), true); + + if (backgroundParallaxContainer != null) + backgroundParallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * BackgroundParallaxAmount; + + ShowOverlays.Value = ShowOverlaysOnEnter; + } + private void onExitingLogo() { logo.AppendAnimatingAction(() => { LogoExiting(logo); }, false); diff --git a/osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs b/osu.Game/Screens/Play/Break/BlurredIcon.cs similarity index 92% rename from osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs rename to osu.Game/Screens/Play/Break/BlurredIcon.cs index 5395d7688e..6f47c97f89 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs +++ b/osu.Game/Screens/Play/Break/BlurredIcon.cs @@ -1,13 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics; -using osu.Game.Graphics; using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using OpenTK; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { public class BlurredIcon : BufferedContainer { diff --git a/osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs b/osu.Game/Screens/Play/Break/BreakArrows.cs similarity index 78% rename from osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs rename to osu.Game/Screens/Play/Break/BreakArrows.cs index 9fdf90bd28..f2a60cdddf 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs +++ b/osu.Game/Screens/Play/Break/BreakArrows.cs @@ -1,18 +1,15 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; -using OpenTK; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; -using osu.Game.Beatmaps.Timing; +using OpenTK; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { - public class ArrowsOverlay : VisibilityContainer + public class BreakArrows : CompositeDrawable { - private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; - private const int glow_icon_size = 60; private const int glow_icon_blur_sigma = 10; private const float glow_icon_final_offset = 0.22f; @@ -29,10 +26,10 @@ namespace osu.Game.Screens.Play.BreaksOverlay private readonly BlurredIcon leftBlurredIcon; private readonly BlurredIcon rightBlurredIcon; - public ArrowsOverlay() + public BreakArrows() { RelativeSizeAxes = Axes.Both; - Children = new Drawable[] + InternalChildren = new Drawable[] { leftGlowIcon = new GlowIcon { @@ -82,22 +79,22 @@ namespace osu.Game.Screens.Play.BreaksOverlay }; } - protected override void PopIn() + public void Show(double duration) { - leftGlowIcon.MoveToX(-glow_icon_final_offset, fade_duration, Easing.OutQuint); - rightGlowIcon.MoveToX(glow_icon_final_offset, fade_duration, Easing.OutQuint); + leftGlowIcon.MoveToX(-glow_icon_final_offset, duration, Easing.OutQuint); + rightGlowIcon.MoveToX(glow_icon_final_offset, duration, Easing.OutQuint); - leftBlurredIcon.MoveToX(-blurred_icon_final_offset, fade_duration, Easing.OutQuint); - rightBlurredIcon.MoveToX(blurred_icon_final_offset, fade_duration, Easing.OutQuint); + leftBlurredIcon.MoveToX(-blurred_icon_final_offset, duration, Easing.OutQuint); + rightBlurredIcon.MoveToX(blurred_icon_final_offset, duration, Easing.OutQuint); } - protected override void PopOut() + public void Hide(double duration) { - leftGlowIcon.MoveToX(-glow_icon_offscreen_offset, fade_duration, Easing.OutQuint); - rightGlowIcon.MoveToX(glow_icon_offscreen_offset, fade_duration, Easing.OutQuint); + leftGlowIcon.MoveToX(-glow_icon_offscreen_offset, duration, Easing.OutQuint); + rightGlowIcon.MoveToX(glow_icon_offscreen_offset, duration, Easing.OutQuint); - leftBlurredIcon.MoveToX(-blurred_icon_offscreen_offset, fade_duration, Easing.OutQuint); - rightBlurredIcon.MoveToX(blurred_icon_offscreen_offset, fade_duration, Easing.OutQuint); + leftBlurredIcon.MoveToX(-blurred_icon_offscreen_offset, duration, Easing.OutQuint); + rightBlurredIcon.MoveToX(blurred_icon_offscreen_offset, duration, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs similarity index 62% rename from osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs rename to osu.Game/Screens/Play/Break/BreakInfo.cs index d7ab4ff2e5..5e011903fe 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.cs @@ -1,24 +1,21 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; -using osu.Game.Beatmaps.Timing; +using OpenTK; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { - public class InfoContainer : VisibilityContainer + public class BreakInfo : Container { - private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; + public PercentageBreakInfoLine AccuracyDisplay; + public BreakInfoLine RankDisplay; + public BreakInfoLine GradeDisplay; - public PercentageInfoLine AccuracyDisplay; - public InfoLine RankDisplay; - public InfoLine GradeDisplay; - - public InfoContainer() + public BreakInfo() { AutoSizeAxes = Axes.Both; Child = new FillFlowContainer @@ -43,16 +40,13 @@ namespace osu.Game.Screens.Play.BreaksOverlay Direction = FillDirection.Vertical, Children = new Drawable[] { - AccuracyDisplay = new PercentageInfoLine("Accuracy"), - RankDisplay = new InfoLine("Rank"), - GradeDisplay = new InfoLine("Grade"), + AccuracyDisplay = new PercentageBreakInfoLine("Accuracy"), + RankDisplay = new BreakInfoLine("Rank"), + GradeDisplay = new BreakInfoLine("Grade"), }, } }, }; } - - protected override void PopIn() => this.FadeIn(fade_duration); - protected override void PopOut() => this.FadeOut(fade_duration); } } diff --git a/osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs b/osu.Game/Screens/Play/Break/BreakInfoLine.cs similarity index 84% rename from osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs rename to osu.Game/Screens/Play/Break/BreakInfoLine.cs index b39eaf1c22..3d96bca1fa 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs +++ b/osu.Game/Screens/Play/Break/BreakInfoLine.cs @@ -8,9 +8,9 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { - public class InfoLine : Container + public class BreakInfoLine : Container where T : struct { private const int margin = 2; @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.BreaksOverlay private readonly string prefix; - public InfoLine(string name, string prefix = @"") + public BreakInfoLine(string name, string prefix = @"") { this.prefix = prefix; @@ -71,9 +71,9 @@ namespace osu.Game.Screens.Play.BreaksOverlay } } - public class PercentageInfoLine : InfoLine + public class PercentageBreakInfoLine : BreakInfoLine { - public PercentageInfoLine(string name, string prefix = "") : base(name, prefix) + public PercentageBreakInfoLine(string name, string prefix = "") : base(name, prefix) { } diff --git a/osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs b/osu.Game/Screens/Play/Break/GlowIcon.cs similarity index 93% rename from osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs rename to osu.Game/Screens/Play/Break/GlowIcon.cs index bad9df2093..79b39a873a 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs +++ b/osu.Game/Screens/Play/Break/GlowIcon.cs @@ -1,13 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics.Containers; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using OpenTK; -using osu.Framework.Allocation; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { public class GlowIcon : Container { @@ -16,24 +16,24 @@ namespace osu.Game.Screens.Play.BreaksOverlay public override Vector2 Size { + get { return base.Size; } set { blurredIcon.Size = spriteIcon.Size = value; blurredIcon.ForceRedraw(); } - get { return base.Size; } } public Vector2 BlurSigma { - set { blurredIcon.BlurSigma = value; } get { return blurredIcon.BlurSigma; } + set { blurredIcon.BlurSigma = value; } } public FontAwesome Icon { - set { spriteIcon.Icon = blurredIcon.Icon = value; } get { return spriteIcon.Icon; } + set { spriteIcon.Icon = blurredIcon.Icon = value; } } public GlowIcon() diff --git a/osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs similarity index 77% rename from osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs rename to osu.Game/Screens/Play/Break/LetterboxOverlay.cs index f4c9362fff..21eb5ebea0 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs +++ b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs @@ -1,18 +1,16 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps.Timing; +using OpenTK.Graphics; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { - public class LetterboxOverlay : VisibilityContainer + public class LetterboxOverlay : CompositeDrawable { - private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; private const int height = 350; private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); @@ -20,7 +18,7 @@ namespace osu.Game.Screens.Play.BreaksOverlay public LetterboxOverlay() { RelativeSizeAxes = Axes.Both; - Children = new Drawable[] + InternalChildren = new Drawable[] { new Container { @@ -48,8 +46,5 @@ namespace osu.Game.Screens.Play.BreaksOverlay } }; } - - protected override void PopIn() => this.FadeIn(fade_duration); - protected override void PopOut() => this.FadeOut(fade_duration); } } diff --git a/osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs b/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs similarity index 70% rename from osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs rename to osu.Game/Screens/Play/Break/RemainingTimeCounter.cs index 015fefb423..f6e683f519 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs +++ b/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs @@ -1,18 +1,15 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Graphics.Sprites; -using osu.Framework.Graphics; using System; -using osu.Game.Beatmaps.Timing; +using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.Sprites; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { public class RemainingTimeCounter : Counter { - private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; - private readonly OsuSpriteText counter; public RemainingTimeCounter() @@ -25,13 +22,8 @@ namespace osu.Game.Screens.Play.BreaksOverlay TextSize = 33, Font = "Venera", }; - - Alpha = 0; } protected override void OnCountChanged(double count) => counter.Text = ((int)Math.Ceiling(count / 1000)).ToString(); - - public override void Show() => this.FadeIn(fade_duration); - public override void Hide() => this.FadeOut(fade_duration); } } diff --git a/osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs similarity index 51% rename from osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs rename to osu.Game/Screens/Play/BreakOverlay.cs index af7c1ef5aa..6c7ee596a1 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -1,15 +1,16 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Scoring; -using System.Collections.Generic; -using osu.Framework.Graphics.UserInterface; +using osu.Game.Screens.Play.Break; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play { public class BreakOverlay : Container { @@ -18,28 +19,26 @@ namespace osu.Game.Screens.Play.BreaksOverlay private const int vertical_margin = 25; private List breaks; + + private readonly Container fadeContainer; + public List Breaks { + get => breaks; set { breaks = value; initializeBreaks(); } - get - { - return breaks; - } } public override bool RemoveCompletedTransforms => false; - private readonly bool letterboxing; - private readonly LetterboxOverlay letterboxOverlay; private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; private readonly RemainingTimeCounter remainingTimeCounter; - private readonly InfoContainer info; - private readonly ArrowsOverlay arrowsOverlay; + private readonly BreakInfo info; + private readonly BreakArrows breakArrows; public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) : this(letterboxing) @@ -49,61 +48,72 @@ namespace osu.Game.Screens.Play.BreaksOverlay public BreakOverlay(bool letterboxing) { - this.letterboxing = letterboxing; - RelativeSizeAxes = Axes.Both; - Children = new Drawable[] + Child = fadeContainer = new Container { - letterboxOverlay = new LetterboxOverlay + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - remainingTimeAdjustmentBox = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Width = 0, - Child = remainingTimeBox = new Container + new LetterboxOverlay + { + Alpha = letterboxing ? 1 : 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + remainingTimeAdjustmentBox = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Height = 8, - CornerRadius = 4, - Masking = true, - Child = new Box { RelativeSizeAxes = Axes.Both } + Width = 0, + Child = remainingTimeBox = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 8, + CornerRadius = 4, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both } + } + }, + remainingTimeCounter = new RemainingTimeCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding { Bottom = vertical_margin }, + }, + info = new BreakInfo + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = vertical_margin }, + }, + breakArrows = new BreakArrows + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, } - }, - remainingTimeCounter = new RemainingTimeCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - Margin = new MarginPadding { Bottom = vertical_margin }, - }, - info = new InfoContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Top = vertical_margin }, - }, - arrowsOverlay = new ArrowsOverlay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, } }; } + protected override void LoadComplete() + { + base.LoadComplete(); + initializeBreaks(); + } + private void initializeBreaks() { + if (!IsLoaded) return; // we need a clock. + FinishTransforms(true); Scheduler.CancelDelayedTasks(); - if (breaks == null) - return; + if (breaks == null) return; //we need breaks. foreach (var b in breaks) { @@ -112,6 +122,9 @@ namespace osu.Game.Screens.Play.BreaksOverlay using (BeginAbsoluteSequence(b.StartTime, true)) { + fadeContainer.FadeIn(fade_duration); + breakArrows.Show(fade_duration); + remainingTimeAdjustmentBox .ResizeWidthTo(remaining_time_container_max_size, fade_duration, Easing.OutQuint) .Delay(b.Duration - fade_duration) @@ -123,37 +136,16 @@ namespace osu.Game.Screens.Play.BreaksOverlay .ResizeWidthTo(1); remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); - } - using (BeginAbsoluteSequence(b.StartTime)) - { - Schedule(showBreak); - using (BeginDelayedSequence(b.Duration - fade_duration)) - Schedule(hideBreak); + using (BeginDelayedSequence(b.Duration - fade_duration, true)) + { + fadeContainer.FadeOut(fade_duration); + breakArrows.Hide(fade_duration); + } } } } - private void showBreak() - { - if (letterboxing) - letterboxOverlay.Show(); - - remainingTimeCounter.Show(); - info.Show(); - arrowsOverlay.Show(); - } - - private void hideBreak() - { - if (letterboxing) - letterboxOverlay.Hide(); - - remainingTimeCounter.Hide(); - info.Hide(); - arrowsOverlay.Hide(); - } - private void bindProcessor(ScoreProcessor processor) { info.AccuracyDisplay.Current.BindTo(processor.Accuracy); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index e68a17f014..b0fbde74d2 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private static bool hasShownNotificationOnce; - public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, DecoupleableInterpolatingFramedClock decoupledClock, WorkingBeatmap working, IAdjustableClock adjustableSourceClock) + public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IClock offsetClock, IAdjustableClock adjustableClock) { RelativeSizeAxes = Axes.Both; @@ -66,13 +66,13 @@ namespace osu.Game.Screens.Play BindRulesetContainer(rulesetContainer); Progress.Objects = rulesetContainer.Objects; - Progress.AudioClock = decoupledClock; + Progress.AudioClock = offsetClock; Progress.AllowSeeking = rulesetContainer.HasReplayLoaded; - Progress.OnSeek = pos => decoupledClock.Seek(pos); + Progress.OnSeek = pos => adjustableClock.Seek(pos); ModDisplay.Current.BindTo(working.Mods); - PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableSourceClock; + PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs index 669bcd600c..40e734b7df 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PauseContainer.cs @@ -44,14 +44,22 @@ namespace osu.Game.Screens.Play public Action OnResume; public Action OnPause; - public IAdjustableClock AudioClock; - public FramedClock FramedClock; + private readonly IAdjustableClock adjustableClock; + private readonly FramedClock framedClock; - public PauseContainer() + public PauseContainer(FramedClock framedClock, IAdjustableClock adjustableClock) { + this.framedClock = framedClock; + this.adjustableClock = adjustableClock; + RelativeSizeAxes = Axes.Both; - AddInternal(content = new Container { RelativeSizeAxes = Axes.Both }); + AddInternal(content = new Container + { + Clock = this.framedClock, + ProcessCustomClock = false, + RelativeSizeAxes = Axes.Both + }); AddInternal(pauseOverlay = new PauseOverlay { @@ -65,47 +73,37 @@ namespace osu.Game.Screens.Play }); } - public void Pause(bool force = false) + public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called. { if (!CanPause && !force) return; if (IsPaused) return; - // stop the decoupled clock (stops the audio eventually) - AudioClock.Stop(); - - // stop processing updatess on the offset clock (instantly freezes time for all our components) - FramedClock.ProcessSourceClockFrames = false; - + // stop the seekable clock (stops the audio eventually) + adjustableClock.Stop(); IsPaused = true; - // we need to do a final check after all of our children have processed up to the paused clock time. - // this is to cover cases where, for instance, the player fails in the current processing frame. - Schedule(() => - { - if (!CanPause) return; + OnPause?.Invoke(); + pauseOverlay.Show(); - lastPauseActionTime = Time.Current; - - OnPause?.Invoke(); - pauseOverlay.Show(); - }); - } + lastPauseActionTime = Time.Current; + }); public void Resume() { if (!IsPaused) return; IsPaused = false; - FramedClock.ProcessSourceClockFrames = true; - + IsResuming = false; lastPauseActionTime = Time.Current; - OnResume?.Invoke(); + // seek back to the time of the framed clock. + // this accounts for the audio clock potentially taking time to enter a completely stopped state. + adjustableClock.Seek(framedClock.CurrentTime); + adjustableClock.Start(); + OnResume?.Invoke(); pauseOverlay.Hide(); - AudioClock.Start(); - IsResuming = false; } private OsuGameBase game; @@ -122,6 +120,9 @@ namespace osu.Game.Screens.Play if (!game.IsActive && CanPause) Pause(); + if (!IsPaused) + framedClock.ProcessFrame(); + base.Update(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4954618ef9..c8ff261a93 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -25,38 +25,39 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Backgrounds; -using osu.Game.Screens.Play.BreaksOverlay; using osu.Game.Screens.Ranking; using osu.Game.Storyboards.Drawables; -using OpenTK; namespace osu.Game.Screens.Play { - public class Player : OsuScreen, IProvideCursor + public class Player : ScreenWithBeatmapBackground, IProvideCursor { - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); + protected override float BackgroundParallaxAmount => 0.1f; public override bool ShowOverlaysOnEnter => false; public Action RestartRequested; - public override bool AllowBeatmapRulesetChange => false; - public bool HasFailed { get; private set; } public bool AllowPause { get; set; } = true; public bool AllowLeadIn { get; set; } = true; public bool AllowResults { get; set; } = true; + private Bindable mouseWheelDisabled; + private Bindable userAudioOffset; + public int RestartCount; public CursorContainer Cursor => RulesetContainer.Cursor; public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value; - private IAdjustableClock adjustableSourceClock; - private FramedOffsetClock offsetClock; - private DecoupleableInterpolatingFramedClock decoupledClock; + private IAdjustableClock sourceClock; + + /// + /// The decoupled clock used for gameplay. Should be used for seeks and clock control. + /// + private DecoupleableInterpolatingFramedClock adjustableClock; private PauseContainer pauseContainer; @@ -64,41 +65,27 @@ namespace osu.Game.Screens.Play private APIAccess api; - private ScoreProcessor scoreProcessor; - protected RulesetContainer RulesetContainer; - - #region User Settings - - private Bindable dimLevel; - private Bindable blurLevel; - private Bindable showStoryboard; - private Bindable mouseWheelDisabled; - private Bindable userAudioOffset; - private SampleChannel sampleRestart; - #endregion - - private Container storyboardContainer; - private DrawableStoryboard storyboard; + private ScoreProcessor scoreProcessor; + protected RulesetContainer RulesetContainer; private HUDOverlay hudOverlay; private FailOverlay failOverlay; + private DrawableStoryboard storyboard; + private Container storyboardContainer; + private bool loadedSuccessfully => RulesetContainer?.Objects.Any() == true; [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuConfigManager config, APIAccess api) + private void load(AudioManager audio, APIAccess api, OsuConfigManager config) { this.api = api; - - dimLevel = config.GetBindable(OsuSetting.DimLevel); - blurLevel = config.GetBindable(OsuSetting.BlurLevel); - showStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); + sampleRestart = audio.Sample.Get(@"Gameplay/restart"); mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); - - sampleRestart = audio.Sample.Get(@"Gameplay/restart"); + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); WorkingBeatmap working = Beatmap.Value; Beatmap beatmap; @@ -138,19 +125,19 @@ namespace osu.Game.Screens.Play return; } - adjustableSourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock(); - decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock(); + adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; var firstObjectTime = RulesetContainer.Objects.First().StartTime; - decoupledClock.Seek(AllowLeadIn + adjustableClock.Seek(AllowLeadIn ? Math.Min(0, firstObjectTime - Math.Max(beatmap.ControlPointInfo.TimingPointAt(firstObjectTime).BeatLength * 4, beatmap.BeatmapInfo.AudioLeadIn)) : firstObjectTime); - decoupledClock.ProcessFrame(); + adjustableClock.ProcessFrame(); - offsetClock = new FramedOffsetClock(decoupledClock); + // the final usable gameplay clock with user-set offsets applied. + var offsetClock = new FramedOffsetClock(adjustableClock); - userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.ValueChanged += v => offsetClock.Offset = v; userAudioOffset.TriggerChange(); @@ -158,16 +145,8 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { - storyboardContainer = new Container + pauseContainer = new PauseContainer(offsetClock, adjustableClock) { - RelativeSizeAxes = Axes.Both, - Clock = offsetClock, - Alpha = 0, - }, - pauseContainer = new PauseContainer - { - AudioClock = decoupledClock, - FramedClock = offsetClock, OnRetry = Restart, OnQuit = Exit, CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded, @@ -179,15 +158,23 @@ namespace osu.Game.Screens.Play OnResume = () => hudOverlay.KeyCounter.IsCounting = true, Children = new Drawable[] { - new Container + storyboardContainer = new Container { RelativeSizeAxes = Axes.Both, - Clock = offsetClock, - Child = RulesetContainer, + Alpha = 0, }, - new SkipButton(firstObjectTime) { AudioClock = decoupledClock }, - hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, decoupledClock, working, adjustableSourceClock) + RulesetContainer, + new SkipButton(firstObjectTime) { + Clock = Clock, // skip button doesn't want to use the audio clock directly + ProcessCustomClock = false, + AdjustableClock = adjustableClock, + FramedClock = offsetClock, + }, + hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, working, offsetClock, adjustableClock) + { + Clock = Clock, // hud overlay doesn't want to use the audio clock directly + ProcessCustomClock = false, Anchor = Anchor.Centre, Origin = Anchor.Centre }, @@ -195,7 +182,7 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Clock = decoupledClock, + ProcessCustomClock = false, Breaks = beatmap.Breaks } } @@ -219,7 +206,7 @@ namespace osu.Game.Screens.Play } }; - if (showStoryboard) + if (ShowStoryboard) initializeStoryboard(false); // Bind ScoreProcessor to ourselves @@ -232,24 +219,11 @@ namespace osu.Game.Screens.Play private void applyRateFromMods() { - if (adjustableSourceClock == null) return; + if (sourceClock == null) return; - adjustableSourceClock.Rate = 1; + sourceClock.Rate = 1; foreach (var mod in Beatmap.Value.Mods.Value.OfType()) - mod.ApplyToClock(adjustableSourceClock); - } - - private void initializeStoryboard(bool asyncLoad) - { - var beatmap = Beatmap.Value; - - storyboard = beatmap.Storyboard.CreateDrawable(Beatmap.Value); - storyboard.Masking = true; - - if (asyncLoad) - LoadComponentAsync(storyboard, storyboardContainer.Add); - else - storyboardContainer.Add(storyboard); + mod.ApplyToClock(sourceClock); } public void Restart() @@ -295,7 +269,7 @@ namespace osu.Game.Screens.Play if (Beatmap.Value.Mods.Value.OfType().Any(m => !m.AllowFail)) return false; - decoupledClock.Stop(); + adjustableClock.Stop(); HasFailed = true; failOverlay.Retries = RestartCount; @@ -310,11 +284,6 @@ namespace osu.Game.Screens.Play if (!loadedSuccessfully) return; - dimLevel.ValueChanged += _ => updateBackgroundElements(); - blurLevel.ValueChanged += _ => updateBackgroundElements(); - showStoryboard.ValueChanged += _ => updateBackgroundElements(); - updateBackgroundElements(); - Content.Alpha = 0; Content .ScaleTo(0.7f) @@ -324,17 +293,19 @@ namespace osu.Game.Screens.Play Task.Run(() => { - adjustableSourceClock.Reset(); + sourceClock.Reset(); Schedule(() => { - decoupledClock.ChangeSource(adjustableSourceClock); + adjustableClock.ChangeSource(sourceClock); applyRateFromMods(); this.Delay(750).Schedule(() => { if (!pauseContainer.IsPaused) - decoupledClock.Start(); + { + adjustableClock.Start(); + } }); }); }); @@ -351,7 +322,7 @@ namespace osu.Game.Screens.Play protected override bool OnExiting(Screen next) { - if ((!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) && (!pauseContainer?.IsResuming ?? false)) + if ((!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) && (!pauseContainer?.IsResuming ?? true)) { // In the case of replays, we may have changed the playback rate. applyRateFromMods(); @@ -361,35 +332,11 @@ namespace osu.Game.Screens.Play } if (loadedSuccessfully) - { pauseContainer?.Pause(); - } return true; } - private void updateBackgroundElements() - { - if (!IsCurrentScreen) return; - - const float duration = 800; - - var opacity = 1 - (float)dimLevel; - - if (showStoryboard && storyboard == null) - initializeStoryboard(true); - - var beatmap = Beatmap.Value; - var storyboardVisible = showStoryboard && beatmap.Storyboard.HasDrawable; - - storyboardContainer - .FadeColour(OsuColour.Gray(opacity), duration, Easing.OutQuint) - .FadeTo(storyboardVisible && opacity > 0 ? 1 : 0, duration, Easing.OutQuint); - - (Background as BackgroundScreenBeatmap)?.BlurTo(new Vector2((float)blurLevel.Value * 25), duration, Easing.OutQuint); - Background?.FadeTo(beatmap.Background != null && (!storyboardVisible || !beatmap.Storyboard.ReplacesBackground) ? opacity : 0, duration, Easing.OutQuint); - } - private void fadeOut() { const float fade_out_duration = 250; @@ -403,5 +350,41 @@ namespace osu.Game.Screens.Play } protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value && !pauseContainer.IsPaused; + + private void initializeStoryboard(bool asyncLoad) + { + if (storyboardContainer == null) + return; + + var beatmap = Beatmap.Value; + + storyboard = beatmap.Storyboard.CreateDrawable(); + storyboard.Masking = true; + + if (asyncLoad) + LoadComponentAsync(storyboard, storyboardContainer.Add); + else + storyboardContainer.Add(storyboard); + } + + protected override void UpdateBackgroundElements() + { + if (!IsCurrentScreen) return; + + base.UpdateBackgroundElements(); + + if (ShowStoryboard && storyboard == null) + initializeStoryboard(true); + + var beatmap = Beatmap.Value; + var storyboardVisible = ShowStoryboard && beatmap.Storyboard.HasDrawable; + + storyboardContainer? + .FadeColour(OsuColour.Gray(BackgroundOpacity), BACKGROUND_FADE_DURATION, Easing.OutQuint) + .FadeTo(storyboardVisible && BackgroundOpacity > 0 ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint); + + if (storyboardVisible && beatmap.Storyboard.ReplacesBackground) + Background?.FadeTo(0, BACKGROUND_FADE_DURATION, Easing.OutQuint); + } } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 2950990779..784dcf7657 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -9,7 +9,6 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Backgrounds; using OpenTK; using osu.Framework.Localisation; using osu.Game.Screens.Menu; @@ -17,7 +16,7 @@ using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Screens.Play { - public class PlayerLoader : OsuScreen + public class PlayerLoader : ScreenWithBeatmapBackground { private Player player; @@ -27,10 +26,6 @@ namespace osu.Game.Screens.Play private bool showOverlays = true; public override bool ShowOverlaysOnEnter => showOverlays; - public override bool AllowBeatmapRulesetChange => false; - - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); - public PlayerLoader(Player player) { this.player = player; @@ -93,8 +88,6 @@ namespace osu.Game.Screens.Play { base.OnEntering(last); - Background.FadeTo(0.4f, 250); - Content.ScaleTo(0.7f); contentIn(); diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index 1a7b80ec9a..6c4d929c71 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -15,7 +15,6 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerSliderBar dimSliderBar; private readonly PlayerSliderBar blurSliderBar; private readonly PlayerCheckbox showStoryboardToggle; - private readonly PlayerCheckbox mouseWheelDisabledToggle; public VisualSettings() { @@ -35,8 +34,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { Text = "Toggles:" }, - showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboards" }, - mouseWheelDisabledToggle = new PlayerCheckbox { LabelText = "Disable mouse wheel" } + showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboards" } }; } @@ -46,7 +44,6 @@ namespace osu.Game.Screens.Play.PlayerSettings dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); showStoryboardToggle.Bindable = config.GetBindable(OsuSetting.ShowStoryboard); - mouseWheelDisabledToggle.Bindable = config.GetBindable(OsuSetting.MouseDisableWheel); } } } diff --git a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs new file mode 100644 index 0000000000..8e963a94a8 --- /dev/null +++ b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Game.Configuration; +using osu.Game.Screens.Backgrounds; +using OpenTK; + +namespace osu.Game.Screens.Play +{ + public abstract class ScreenWithBeatmapBackground : OsuScreen + { + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); + + public override bool AllowBeatmapRulesetChange => false; + + protected const float BACKGROUND_FADE_DURATION = 800; + + protected float BackgroundOpacity => 1 - (float)DimLevel; + + #region User Settings + + protected Bindable DimLevel; + protected Bindable BlurLevel; + protected Bindable ShowStoryboard; + + #endregion + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + DimLevel = config.GetBindable(OsuSetting.DimLevel); + BlurLevel = config.GetBindable(OsuSetting.BlurLevel); + ShowStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); + } + + protected override void OnEntering(Screen last) + { + base.OnEntering(last); + DimLevel.ValueChanged += _ => UpdateBackgroundElements(); + BlurLevel.ValueChanged += _ => UpdateBackgroundElements(); + ShowStoryboard.ValueChanged += _ => UpdateBackgroundElements(); + UpdateBackgroundElements(); + } + + protected virtual void UpdateBackgroundElements() + { + if (!IsCurrentScreen) return; + + Background?.FadeTo(BackgroundOpacity, BACKGROUND_FADE_DURATION, Easing.OutQuint); + (Background as BackgroundScreenBeatmap)?.BlurTo(new Vector2((float)BlurLevel.Value * 25), BACKGROUND_FADE_DURATION, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/Play/SkipButton.cs b/osu.Game/Screens/Play/SkipButton.cs index f67a9b801e..08bb26c72b 100644 --- a/osu.Game/Screens/Play/SkipButton.cs +++ b/osu.Game/Screens/Play/SkipButton.cs @@ -24,7 +24,9 @@ namespace osu.Game.Screens.Play public class SkipButton : OverlayContainer, IKeyBindingHandler { private readonly double startTime; - public IAdjustableClock AudioClock; + + public IAdjustableClock AdjustableClock; + public IFrameBasedClock FramedClock; private Button button; private Box remainingTimeBox; @@ -60,8 +62,11 @@ namespace osu.Game.Screens.Play { var baseClock = Clock; - if (AudioClock != null) - Clock = new FramedClock(AudioClock) { ProcessSourceClockFrames = false }; + if (FramedClock != null) + { + Clock = FramedClock; + ProcessCustomClock = false; + } Children = new Drawable[] { @@ -109,7 +114,7 @@ namespace osu.Game.Screens.Play using (BeginAbsoluteSequence(beginFadeTime)) this.FadeOut(fade_time); - button.Action = () => AudioClock?.Seek(startTime - skip_required_cutoff - fade_time); + button.Action = () => AdjustableClock?.Seek(startTime - skip_required_cutoff - fade_time); displayTime = Time.Current; diff --git a/osu.Game/Screens/Play/SongProgressBar.cs b/osu.Game/Screens/Play/SongProgressBar.cs index ffe7ae04f8..4f5cc79b53 100644 --- a/osu.Game/Screens/Play/SongProgressBar.cs +++ b/osu.Game/Screens/Play/SongProgressBar.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play { var xFill = value * UsableWidth; fill.Width = xFill; - handleBase.MoveToX(xFill); + handleBase.X = xFill; } protected override void OnUserChange() => OnSeek?.Invoke(Current); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6a6042d7d4..c2bb155753 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -169,20 +169,43 @@ namespace osu.Game.Screens.Select }); } - public void SelectBeatmap(BeatmapInfo beatmap) + /// + /// Selects a given beatmap on the carousel. + /// + /// If bypassFilters is false, we will try to select another unfiltered beatmap in the same set. If the + /// entire set is filtered, no selection is made. + /// + /// The beatmap to select. + /// Whether to select the beatmap even if it is filtered (i.e., not visible on carousel). + /// True if a selection was made, False if it wasn't. + public bool SelectBeatmap(BeatmapInfo beatmap, bool bypassFilters = true) { if (beatmap?.Hidden != false) - return; + return false; - foreach (CarouselBeatmapSet group in beatmapSets) + foreach (CarouselBeatmapSet set in beatmapSets) { - var item = group.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + if (!bypassFilters && set.Filtered) + continue; + + var item = set.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + + if (item == null) + // The beatmap that needs to be selected doesn't exist in this set + continue; + + if (!bypassFilters && item.Filtered) + // The beatmap exists in this set but is filtered, so look for the first unfiltered map in the set + item = set.Beatmaps.FirstOrDefault(b => !b.Filtered); + if (item != null) { select(item); - return; + return true; } } + + return false; } /// @@ -192,7 +215,9 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (!Items.Any()) + var visibleItems = Items.Where(s => !s.Item.Filtered).ToList(); + + if (!visibleItems.Any()) return; DrawableCarouselItem drawable = null; @@ -202,15 +227,15 @@ namespace osu.Game.Screens.Select // we can fix this by changing this method to not reference drawables / Items in the first place. return; - int originalIndex = Items.IndexOf(drawable); + int originalIndex = visibleItems.IndexOf(drawable); int currentIndex = originalIndex; // local function to increment the index in the required direction, wrapping over extremities. - int incrementIndex() => currentIndex = (currentIndex + direction + Items.Count) % Items.Count; + int incrementIndex() => currentIndex = (currentIndex + direction + visibleItems.Count) % visibleItems.Count; while (incrementIndex() != originalIndex) { - var item = Items[currentIndex].Item; + var item = visibleItems[currentIndex].Item; if (item.Filtered || item.State == CarouselItemState.Selected) continue; @@ -407,12 +432,14 @@ namespace osu.Game.Screens.Select continue; } + float depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0); + // Only add if we're not already part of the content. if (!scrollableContent.Contains(item)) { // Makes sure headers are always _below_ items, // and depth flows downward. - item.Depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0); + item.Depth = depth; switch (item.LoadState) { @@ -426,6 +453,10 @@ namespace osu.Game.Screens.Select break; } } + else + { + scrollableContent.ChangeChildDepth(item, depth); + } } // this is not actually useful right now, but once we have groups may well be. diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 21e6108489..be83d7b500 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -41,19 +41,25 @@ namespace osu.Game.Screens.Select /// Higher depth to be put on the left, and lower to be put on the right. /// Notice this is different to ! /// - public void AddButton(string text, Color4 colour, Action action, Key? hotkey = null, float depth = 0) => buttons.Add(new FooterButton + public void AddButton(string text, Color4 colour, Action action, Key? hotkey = null, float depth = 0) { - Text = text, - Height = play_song_select_button_height, - Width = play_song_select_button_width, - Depth = depth, - SelectedColour = colour, - DeselectedColour = colour.Opacity(0.5f), - Hotkey = hotkey, - Hovered = updateModeLight, - HoverLost = updateModeLight, - Action = action, - }); + var button = new FooterButton + { + Text = text, + Height = play_song_select_button_height, + Width = play_song_select_button_width, + Depth = depth, + SelectedColour = colour, + DeselectedColour = colour.Opacity(0.5f), + Hotkey = hotkey, + Hovered = updateModeLight, + HoverLost = updateModeLight, + Action = action, + }; + + buttons.Add(button); + buttons.SetLayoutPosition(button, -depth); + } private readonly List overlays = new List(); diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index 2e8b2f9014..dee1ec4511 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Select.Options /// public void AddButton(string firstLine, string secondLine, FontAwesome icon, Color4 colour, Action action, Key? hotkey = null, float depth = 0) { - buttonsContainer.Add(new BeatmapOptionsButton + var button = new BeatmapOptionsButton { FirstLineText = firstLine, SecondLineText = secondLine, @@ -108,7 +108,10 @@ namespace osu.Game.Screens.Select.Options action?.Invoke(); }, HotKey = hotkey - }); + }; + + buttonsContainer.Add(button); + buttonsContainer.SetLayoutPosition(button, depth); } } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 739bc39269..c347bfe70f 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -1,11 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using System.Linq; using OpenTK.Input; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; @@ -47,13 +49,15 @@ namespace osu.Game.Screens.Select private SampleChannel sampleConfirm; - [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, OsuGame game) - { - sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); + public readonly Bindable> SelectedMods = new Bindable>(new List()); - if (game != null) - modSelect.SelectedMods.BindTo(game.SelectedMods); + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, OsuGame osu) + { + if (osu != null) SelectedMods.BindTo(osu.SelectedMods); + modSelect.SelectedMods.BindTo(SelectedMods); + + sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); Footer.AddButton(@"mods", colours.Yellow, modSelect, Key.F1, float.MaxValue); @@ -80,7 +84,7 @@ namespace osu.Game.Screens.Select { base.UpdateBeatmap(beatmap); - beatmap.Mods.BindTo(modSelect.SelectedMods); + beatmap.Mods.BindTo(SelectedMods); BeatmapDetails.Beatmap = beatmap; @@ -95,7 +99,7 @@ namespace osu.Game.Screens.Select if (removeAutoModOnResume) { var autoType = Ruleset.Value.CreateInstance().GetAutoplayMod().GetType(); - modSelect.SelectedMods.Value = modSelect.SelectedMods.Value.Where(m => m.GetType() != autoType).ToArray(); + SelectedMods.Value = SelectedMods.Value.Where(m => m.GetType() != autoType).ToArray(); removeAutoModOnResume = false; } @@ -125,7 +129,7 @@ namespace osu.Game.Screens.Select if (Beatmap.Value.Track != null) Beatmap.Value.Track.Looping = false; - Beatmap.Value.Mods.UnbindBindings(); + SelectedMods.UnbindAll(); Beatmap.Value.Mods.Value = new Mod[] { }; return false; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index de6847d866..ca8a1cae41 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -214,11 +214,7 @@ namespace osu.Game.Screens.Select Beatmap.DisabledChanged += disabled => Carousel.AllowSelection = !disabled; Beatmap.TriggerChange(); - Beatmap.ValueChanged += b => - { - if (IsCurrentScreen) - Carousel.SelectBeatmap(b?.BeatmapInfo); - }; + Beatmap.ValueChanged += workingBeatmapChanged; } public void Edit(BeatmapInfo beatmap) @@ -261,6 +257,17 @@ namespace osu.Game.Screens.Select // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. private BeatmapInfo beatmapNoDebounce; + private void workingBeatmapChanged(WorkingBeatmap beatmap) + { + if (IsCurrentScreen && !Carousel.SelectBeatmap(beatmap?.BeatmapInfo, false)) + // If selecting new beatmap without bypassing filters failed, there's possibly a ruleset mismatch + if (beatmap?.BeatmapInfo?.Ruleset != null && beatmap.BeatmapInfo.Ruleset != Ruleset.Value) + { + Ruleset.Value = beatmap.BeatmapInfo.Ruleset; + Carousel.SelectBeatmap(beatmap.BeatmapInfo); + } + } + /// /// selection has been changed as the result of interaction with the carousel. /// @@ -386,6 +393,8 @@ namespace osu.Game.Screens.Select protected override bool OnExiting(Screen next) { + FinaliseSelection(); + beatmapInfoWedge.State = Visibility.Hidden; Content.FadeOut(100); @@ -448,16 +457,14 @@ namespace osu.Game.Screens.Select private void carouselBeatmapsLoaded() { - if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false) + if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) + return; + + if (Carousel.SelectedBeatmapSet == null && !Carousel.SelectNextRandom()) { - Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo); - } - else if (Carousel.SelectedBeatmapSet == null) - { - if (!Carousel.SelectNextRandom()) - // in the case random selection failed, we want to trigger selectionChanged - // to show the dummy beatmap (we have nothing else to display). - carouselSelectionChanged(null); + // in the case random selection failed, we want to trigger selectionChanged + // to show the dummy beatmap (we have nothing else to display). + carouselSelectionChanged(null); } } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs new file mode 100644 index 0000000000..e40a43d400 --- /dev/null +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; + +namespace osu.Game.Skinning +{ + public class DefaultSkin : Skin + { + public DefaultSkin() + : base(SkinInfo.Default) + { + } + + public override Drawable GetDrawableComponent(string componentName) + { + return null; + } + + public override SampleChannel GetSample(string sampleName) + { + return null; + } + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs new file mode 100644 index 0000000000..17fe6369a7 --- /dev/null +++ b/osu.Game/Skinning/LegacySkin.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.IO; +using System.Linq; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; + +namespace osu.Game.Skinning +{ + public class LegacySkin : Skin + { + private readonly TextureStore textures; + + private readonly SampleManager samples; + + public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) + : base(skin) + { + storage = new LegacySkinResourceStore(skin, storage); + samples = audioManager.GetSampleManager(storage); + textures = new TextureStore(new RawTextureLoaderStore(storage)); + } + + public override Drawable GetDrawableComponent(string componentName) + { + var texture = textures.Get(componentName); + if (texture == null) return null; + + return new Sprite + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Texture = texture, + }; + } + + public override SampleChannel GetSample(string sampleName) => samples.Get(sampleName); + + private class LegacySkinResourceStore : IResourceStore + { + private readonly SkinInfo skin; + private readonly IResourceStore underlyingStore; + + private string getPathForFile(string filename) => + skin.Files.FirstOrDefault(f => string.Equals(Path.GetFileNameWithoutExtension(f.Filename), filename.Split('/').Last(), StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; + + public LegacySkinResourceStore(SkinInfo skin, IResourceStore underlyingStore) + { + this.skin = skin; + this.underlyingStore = underlyingStore; + } + + public Stream GetStream(string name) => underlyingStore.GetStream(getPathForFile(name)); + + byte[] IResourceStore.Get(string name) => underlyingStore.Get(getPathForFile(name)); + } + } +} diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs new file mode 100644 index 0000000000..fafbdec8f0 --- /dev/null +++ b/osu.Game/Skinning/Skin.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; + +namespace osu.Game.Skinning +{ + public abstract class Skin + { + public readonly SkinInfo SkinInfo; + + public abstract Drawable GetDrawableComponent(string componentName); + + public abstract SampleChannel GetSample(string sampleName); + + protected Skin(SkinInfo skin) + { + SkinInfo = skin; + } + } +} diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 0031968b2b..88d51eca10 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; +using osu.Framework.Audio; using osu.Framework.Configuration; using osu.Framework.Platform; using osu.Game.Database; @@ -15,6 +16,9 @@ namespace osu.Game.Skinning { public class SkinManager : ArchiveModelManager { + private readonly AudioManager audio; + + public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin()); public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; public override string[] HandledExtensions => new[] { ".osk" }; @@ -30,13 +34,37 @@ namespace osu.Game.Skinning return userSkins; } - protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name }; + protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo + { + Name = archive.Name + }; + + /// + /// Retrieve a instance for the provided + /// + /// The skin to lookup. + /// A instance correlating to the provided . + public Skin GetSkin(SkinInfo skinInfo) + { + if (skinInfo == SkinInfo.Default) + return new DefaultSkin(); + + return new LegacySkin(skinInfo, Files.Store, audio); + } private SkinStore store; - public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost) + public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost, AudioManager audio) : base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost) { + this.audio = audio; + + CurrentSkinInfo.ValueChanged += info => CurrentSkin.Value = GetSkin(info); + CurrentSkin.ValueChanged += skin => + { + if (skin.SkinInfo != CurrentSkinInfo.Value) + throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead."); + }; } /// diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs new file mode 100644 index 0000000000..3e33f952cd --- /dev/null +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Skinning +{ + /// + /// A drawable which has a callback when the skin changes. + /// + public abstract class SkinReloadableDrawable : CompositeDrawable + { + private Bindable skin; + + /// + /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. + /// + private readonly bool allowDefaultFallback; + + /// + /// Create a new + /// + /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. + protected SkinReloadableDrawable(bool fallback = true) + { + allowDefaultFallback = fallback; + } + + [BackgroundDependencyLoader] + private void load(SkinManager skinManager) + { + skin = skinManager.CurrentSkin.GetBoundCopy(); + skin.ValueChanged += skin => SkinChanged(skin, allowDefaultFallback || skin.SkinInfo == SkinInfo.Default); + } + + protected override void LoadAsyncComplete() + { + base.LoadAsyncComplete(); + skin.TriggerChange(); + } + + /// + /// Called when a change is made to the skin. + /// + /// The new skin. + /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. + protected virtual void SkinChanged(Skin skin, bool allowFallback) + { + } + } +} diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs new file mode 100644 index 0000000000..cd669778a6 --- /dev/null +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics; + +namespace osu.Game.Skinning +{ + public class SkinnableDrawable : SkinnableDrawable + { + public SkinnableDrawable(string name, Func defaultImplementation, bool fallback = true) + : base(name, defaultImplementation, fallback) + { + } + } + + public class SkinnableDrawable : SkinReloadableDrawable + where T : Drawable + { + private readonly Func createDefault; + + private readonly string componentName; + + public SkinnableDrawable(string name, Func defaultImplementation, bool fallback = true) : base(fallback) + { + componentName = name; + createDefault = defaultImplementation; + + RelativeSizeAxes = Axes.Both; + } + + protected override void SkinChanged(Skin skin, bool allowFallback) + { + var drawable = skin.GetDrawableComponent(componentName); + if (drawable == null && allowFallback) + drawable = createDefault(componentName); + + if (drawable != null) + InternalChild = drawable; + else + ClearInternal(); + } + } +} diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs new file mode 100644 index 0000000000..fd52d62d59 --- /dev/null +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + public class SkinnableSound : SkinReloadableDrawable + { + private readonly SampleInfo[] samples; + private SampleChannel[] channels; + + private AudioManager audio; + + public SkinnableSound(params SampleInfo[] samples) + { + this.samples = samples; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + this.audio = audio; + } + + public void Play() => channels?.ForEach(c => c.Play()); + + protected override void SkinChanged(Skin skin, bool allowFallback) + { + channels = samples.Select(s => + { + var ch = loadChannel(s, skin.GetSample); + if (ch == null && allowFallback) + ch = loadChannel(s, audio.Sample.Get); + return ch; + }).Where(c => c != null).ToArray(); + } + + private SampleChannel loadChannel(SampleInfo info, Func getSampleFunction) + { + SampleChannel ch = null; + + if (info.Namespace != null) + ch = getSampleFunction($"Gameplay/{info.Namespace}/{info.Bank}-{info.Name}"); + + // try without namespace as a fallback. + if (ch == null) + ch = getSampleFunction($"Gameplay/{info.Bank}-{info.Name}"); + + if (ch != null) + ch.Volume.Value = info.Volume / 100.0; + + return ch; + } + } +} diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index aaeaaabd55..9da92d8cb4 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -33,6 +33,8 @@ namespace osu.Game.Storyboards.Drawables } } + public override bool RemoveCompletedTransforms => false; + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index ef782abbe5..0b84ff3297 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -17,6 +17,8 @@ namespace osu.Game.Storyboards.Drawables public bool FlipH { get; set; } public bool FlipV { get; set; } + public override bool RemoveWhenNotAlive => false; + protected override Vector2 DrawScale => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index a39805f74e..c4b9a3d47e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -17,6 +17,8 @@ namespace osu.Game.Storyboards.Drawables public bool FlipH { get; set; } public bool FlipV { get; set; } + public override bool RemoveWhenNotAlive => false; + protected override Vector2 DrawScale => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs new file mode 100644 index 0000000000..219d805bc1 --- /dev/null +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -0,0 +1,144 @@ +// Copyright (c) 2007-2018 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.Reflection; +using Newtonsoft.Json; +using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Tests.Beatmaps +{ + [TestFixture] + public abstract class BeatmapConversionTest + where TConvertValue : IEquatable + { + private const string resource_namespace = "Testing.Beatmaps"; + private const string expected_conversion_suffix = "-expected-conversion"; + + protected abstract string ResourceAssembly { get; } + + protected void Test(string name) + { + var ourResult = convert(name); + var expectedResult = read(name); + + Assert.Multiple(() => + { + int mappingCounter = 0; + while (true) + { + if (mappingCounter >= ourResult.Mappings.Count && mappingCounter >= expectedResult.Mappings.Count) + break; + if (mappingCounter >= ourResult.Mappings.Count) + Assert.Fail($"A conversion did not generate any hitobjects, but should have, for hitobject at time: {expectedResult.Mappings[mappingCounter].StartTime}\n"); + else if (mappingCounter >= expectedResult.Mappings.Count) + Assert.Fail($"A conversion generated hitobjects, but should not have, for hitobject at time: {ourResult.Mappings[mappingCounter].StartTime}\n"); + else + { + var counter = mappingCounter; + Assert.Multiple(() => + { + var ourMapping = ourResult.Mappings[counter]; + var expectedMapping = expectedResult.Mappings[counter]; + + int objectCounter = 0; + while (true) + { + if (objectCounter >= ourMapping.Objects.Count && objectCounter >= expectedMapping.Objects.Count) + break; + if (objectCounter >= ourMapping.Objects.Count) + Assert.Fail($"The conversion did not generate a hitobject, but should have, for hitobject at time: {expectedMapping.StartTime}:\n" + + $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n"); + else if (objectCounter >= expectedMapping.Objects.Count) + Assert.Fail($"The conversion generated a hitobject, but should not have, for hitobject at time: {ourMapping.StartTime}:\n" + + $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n"); + else if (!EqualityComparer.Default.Equals(expectedMapping.Objects[objectCounter], ourMapping.Objects[objectCounter])) + { + Assert.Fail($"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}\n" + + $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n" + + $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n"); + } + + objectCounter++; + } + }); + } + + mappingCounter++; + } + }); + } + + private ConvertResult convert(string name) + { + var beatmap = getBeatmap(name); + + var result = new ConvertResult(); + + var converter = CreateConverter(beatmap); + converter.ObjectConverted += (orig, converted) => + { + converted.ForEach(h => h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty)); + + var mapping = new ConvertMapping { StartTime = orig.StartTime }; + foreach (var obj in converted) + mapping.Objects.AddRange(CreateConvertValue(obj)); + result.Mappings.Add(mapping); + }; + + converter.Convert(beatmap); + + return result; + } + + private ConvertResult read(string name) + { + using (var resStream = openResource($"{resource_namespace}.{name}{expected_conversion_suffix}.json")) + using (var reader = new StreamReader(resStream)) + { + var contents = reader.ReadToEnd(); + return JsonConvert.DeserializeObject(contents); + } + } + + private Beatmap getBeatmap(string name) + { + using (var resStream = openResource($"{resource_namespace}.{name}.osu")) + using (var stream = new StreamReader(resStream)) + { + var decoder = Decoder.GetDecoder(stream); + ((LegacyBeatmapDecoder)decoder).ApplyOffsets = false; + return decoder.DecodeBeatmap(stream); + } + } + + private Stream openResource(string name) + { + var localPath = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path)); + return Assembly.LoadFrom(Path.Combine(localPath, $"{ResourceAssembly}.dll")).GetManifestResourceStream($@"{ResourceAssembly}.Resources.{name}"); + } + + protected abstract IEnumerable CreateConvertValue(HitObject hitObject); + protected abstract IBeatmapConverter CreateConverter(Beatmap beatmap); + + private class ConvertMapping + { + [JsonProperty] + public double StartTime; + [JsonProperty] + public List Objects = new List(); + } + + private class ConvertResult + { + [JsonProperty] + public List Mappings = new List(); + } + } +} diff --git a/osu.Game/Tests/TestTestCase.cs b/osu.Game/Tests/TestTestCase.cs new file mode 100644 index 0000000000..4efd57095e --- /dev/null +++ b/osu.Game/Tests/TestTestCase.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Testing; + +namespace osu.Game.Tests +{ + [TestFixture] + internal class TestTestCase : TestCase + { + // This TestCase is required for nunit to not throw errors + // See: https://github.com/nunit/nunit/issues/1118 + } +} diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs index 5b32433467..7ca69c14b8 100644 --- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs +++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs @@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual private void load(BeatmapManager beatmaps) { var sets = beatmaps.GetAllUsableBeatmapSets(); - var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID < 0 || b.RulesetID == ruleset.LegacyID); + var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID == null || b.RulesetID == ruleset.LegacyID); allBeatmaps.ForEach(b => beatmapDisplays.Add(new BeatmapDisplay(b))); } diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index c29bc91d17..2504c9c62c 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -73,10 +73,10 @@ namespace osu.Game.Users public struct UserRanks { [JsonProperty(@"global")] - public int Global; + public int? Global; [JsonProperty(@"country")] - public int Country; + public int? Country; } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 829addc360..1d3baa6c0d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -270,6 +270,7 @@ + @@ -286,6 +287,7 @@ + @@ -293,12 +295,16 @@ + 20180125143340_Settings.cs + + + @@ -323,7 +329,7 @@ 20171209034410_AddRulesetInfoShortName.cs - + 20180219060912_AddSkins.cs @@ -350,9 +356,16 @@ + + + + + + + @@ -361,10 +374,17 @@ - + + + + + + + + @@ -391,14 +411,14 @@ - - - - - - - - + + + + + + + + @@ -465,9 +485,6 @@ - - - @@ -706,8 +723,6 @@ - - @@ -854,9 +869,15 @@ + + + + + + @@ -874,9 +895,11 @@ + +