diff --git a/.vscode/launch.json b/.vscode/launch.json index 0e07b0a067..b3b86da42f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,46 +2,60 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch VisualTests", + "name": "VisualTests (debug)", "windows": { "type": "clr" }, "type": "mono", "request": "launch", "program": "${workspaceRoot}/osu.Desktop.VisualTests/bin/Debug/osu!.exe", - "args": [], "cwd": "${workspaceRoot}", - "preLaunchTask": "build", + "preLaunchTask": "Build (Debug)", "runtimeExecutable": null, "env": {}, "console": "internalConsole" }, { - "name": "Launch Desktop", + "name": "VisualTests (release)", + "windows": { + "type": "clr" + }, + "type": "mono", + "request": "launch", + "program": "${workspaceRoot}/osu.Desktop.VisualTests/bin/Release/osu!.exe", + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Release)", + "runtimeExecutable": null, + "env": {}, + "console": "internalConsole" + }, + { + "name": "osu! (debug)", "windows": { "type": "clr" }, "type": "mono", "request": "launch", "program": "${workspaceRoot}/osu.Desktop/bin/Debug/osu!.exe", - "args": [], "cwd": "${workspaceRoot}", - "preLaunchTask": "build", + "preLaunchTask": "Build (Debug)", "runtimeExecutable": null, "env": {}, "console": "internalConsole" }, { - "name": "Attach", + "name": "osu! (release)", "windows": { - "type": "clr", - "request": "attach", - "processName": "osu!" + "type": "clr" }, "type": "mono", - "request": "attach", - "address": "localhost", - "port": 55555 + "request": "launch", + "program": "${workspaceRoot}/osu.Desktop/bin/Release/osu!.exe", + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Release)", + "runtimeExecutable": null, + "env": {}, + "console": "internalConsole" } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5eaeaa9899..f285ebde67 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,51 +1,50 @@ { // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format - "version": "0.1.0", - "taskSelector": "/t:", + "version": "2.0.0", + "problemMatcher": "$msCompile", + "isShellCommand": true, + "command": "msbuild", + "suppressTaskName": true, + "showOutput": "silent", + "args": [ + "/property:GenerateFullPaths=true", + "/property:DebugType=portable" + ], + "windows": { + "args": [ + "/property:GenerateFullPaths=true", + "/property:DebugType=portable", + "/m" //parallel compiling support. doesn't work well with mono atm + ] + }, "tasks": [ { - "taskName": "build", - "isShellCommand": true, - "showOutput": "silent", - "command": "msbuild", - "args": [ - "/property:GenerateFullPaths=true", - "/property:DebugType=portable" - ], - "windows": { - "args": [ - "/property:GenerateFullPaths=true", - "/property:DebugType=portable", - "/m" //parallel compiling support. doesn't work well with mono atm - ] - }, - // Use the standard MS compiler pattern to detect errors, warnings and infos - "problemMatcher": "$msCompile", + "taskName": "Build (Debug)", "isBuildCommand": true }, { - "taskName": "rebuild", - "isShellCommand": true, - "showOutput": "silent", - "command": "msbuild", + "taskName": "Build (Release)", "args": [ - // Ask msbuild to generate full paths for file names. - "/property:GenerateFullPaths=true", - "/property:DebugType=portable", - "/target:Clean,Build" - ], - "windows": { - "args": [ - "/property:GenerateFullPaths=true", - "/property:DebugType=portable", - "/target:Clean,Build", - "/m" //parallel compiling support. doesn't work well with mono atm - ] - }, - // Use the standard MS compiler pattern to detect errors, warnings and infos - "problemMatcher": "$msCompile", - "isBuildCommand": true + "/property:Configuration=Release" + ] + }, + { + "taskName": "Clean All", + "dependsOn": ["Clean (Debug)", "Clean (Release)"] + }, + { + "taskName": "Clean (Debug)", + "args": [ + "/target:Clean" + ] + }, + { + "taskName": "Clean (Release)", + "args": [ + "/target:Clean", + "/property:Configuration=Release" + ] } ] } \ No newline at end of file diff --git a/osu-framework b/osu-framework index 58c108309f..60e0a343d2 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 58c108309f72462d8dc45aa488ab78cd6db08cf1 +Subproject commit 60e0a343d2bf590f736782e2bb2a01c132e6cac0 diff --git a/osu-resources b/osu-resources index b90c4ed490..ffccbeb98d 160000 --- a/osu-resources +++ b/osu-resources @@ -1 +1 @@ -Subproject commit b90c4ed490f76f2995662b3a8af3a32b8756a012 +Subproject commit ffccbeb98dc9e8f0965520270b5885e63f244c83 diff --git a/osu.Desktop.VisualTests/Beatmaps/TestWorkingBeatmap.cs b/osu.Desktop.VisualTests/Beatmaps/TestWorkingBeatmap.cs index 5e3f5b5133..b45574b761 100644 --- a/osu.Desktop.VisualTests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Desktop.VisualTests/Beatmaps/TestWorkingBeatmap.cs @@ -10,7 +10,7 @@ namespace osu.Desktop.VisualTests.Beatmaps public class TestWorkingBeatmap : WorkingBeatmap { public TestWorkingBeatmap(Beatmap beatmap) - : base(beatmap.BeatmapInfo, beatmap.BeatmapInfo.BeatmapSet) + : base(beatmap.BeatmapInfo) { this.beatmap = beatmap; } diff --git a/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs b/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs index 4a59ad9534..58cbad936a 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Framework.Graphics.Primitives; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Screens.Select; diff --git a/osu.Desktop.VisualTests/Tests/TestCaseDrawings.cs b/osu.Desktop.VisualTests/Tests/TestCaseDrawings.cs index a0463516de..ebc9930f93 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseDrawings.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseDrawings.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using osu.Framework.Testing; using osu.Game.Screens.Tournament; using osu.Game.Screens.Tournament.Teams; -using osu.Game.Users; namespace osu.Desktop.VisualTests.Tests { @@ -25,57 +24,57 @@ namespace osu.Desktop.VisualTests.Tests private class TestTeamList : ITeamList { - public IEnumerable Teams { get; } = new[] + public IEnumerable Teams { get; } = new[] { - new Country + new DrawingsTeam { FlagName = "GB", FullName = "United Kingdom", Acronym = "UK" }, - new Country + new DrawingsTeam { FlagName = "FR", FullName = "France", Acronym = "FRA" }, - new Country + new DrawingsTeam { FlagName = "CN", FullName = "China", Acronym = "CHN" }, - new Country + new DrawingsTeam { FlagName = "AU", FullName = "Australia", Acronym = "AUS" }, - new Country + new DrawingsTeam { FlagName = "JP", FullName = "Japan", Acronym = "JPN" }, - new Country + new DrawingsTeam { FlagName = "RO", FullName = "Romania", Acronym = "ROM" }, - new Country + new DrawingsTeam { FlagName = "IT", FullName = "Italy", Acronym = "PIZZA" }, - new Country + new DrawingsTeam { FlagName = "VE", FullName = "Venezuela", Acronym = "VNZ" }, - new Country + new DrawingsTeam { FlagName = "US", FullName = "United States of America", diff --git a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs b/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs index cb15558ec3..6bd9d35b80 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Taiko.UI; using System.Collections.Generic; using osu.Desktop.VisualTests.Beatmaps; using osu.Framework.Allocation; +using osu.Game.Beatmaps.Timing; namespace osu.Desktop.VisualTests.Tests { @@ -52,6 +53,12 @@ namespace osu.Desktop.VisualTests.Tests time += RNG.Next(50, 500); } + TimingInfo timing = new TimingInfo(); + timing.ControlPoints.Add(new ControlPoint + { + BeatLength = 200 + }); + WorkingBeatmap beatmap = new TestWorkingBeatmap(new Beatmap { HitObjects = objects, @@ -64,8 +71,9 @@ namespace osu.Desktop.VisualTests.Tests Artist = @"Unknown", Title = @"Sample Beatmap", Author = @"peppy", - } - } + }, + }, + TimingInfo = timing }); Add(new Drawable[] @@ -77,25 +85,25 @@ namespace osu.Desktop.VisualTests.Tests Clock = new FramedClock(), Children = new Drawable[] { - new OsuHitRenderer(beatmap) + new OsuHitRenderer(beatmap, false) { Scale = new Vector2(0.5f), Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft }, - new TaikoHitRenderer(beatmap) + new TaikoHitRenderer(beatmap, false) { Scale = new Vector2(0.5f), Anchor = Anchor.TopRight, Origin = Anchor.TopRight }, - new CatchHitRenderer(beatmap) + new CatchHitRenderer(beatmap, false) { Scale = new Vector2(0.5f), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft }, - new ManiaHitRenderer(beatmap) + new ManiaHitRenderer(beatmap, false) { Scale = new Vector2(0.5f), Anchor = Anchor.BottomRight, diff --git a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs index dceb7a9cff..8c913ae95e 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs @@ -129,8 +129,6 @@ namespace osu.Desktop.VisualTests.Tests }; Add(clockAdjustContainer); - - load(mode); } private int depth; diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseManiaHitObjects.cs new file mode 100644 index 0000000000..3113b63db1 --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseManiaHitObjects.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2007-2017 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.Testing; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using OpenTK.Graphics; +using OpenTK; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseManiaHitObjects : TestCase + { + public override void Reset() + { + base.Reset(); + + Add(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + // Imagine that the containers containing the drawable notes are the "columns" + Children = new Drawable[] + { + new Container + { + Name = "Normal note column", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 50, + Children = new[] + { + new Container + { + Name = "Timing section", + RelativeSizeAxes = Axes.Both, + RelativeCoordinateSpace = new Vector2(1, 10000), + Children = new[] + { + new DrawableNote(new Note { StartTime = 5000 }) { AccentColour = Color4.Red }, + new DrawableNote(new Note { StartTime = 6000 }) { AccentColour = Color4.Red } + } + } + } + }, + new Container + { + Name = "Hold note column", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 50, + Children = new[] + { + new Container + { + Name = "Timing section", + RelativeSizeAxes = Axes.Both, + RelativeCoordinateSpace = new Vector2(1, 10000), + Children = new[] + { + new DrawableHoldNote(new HoldNote + { + StartTime = 5000, + Duration = 1000 + }) { AccentColour = Color4.Red } + } + } + } + } + } + }); + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs new file mode 100644 index 0000000000..04fcd8e94a --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs @@ -0,0 +1,98 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.UI; +using System; +using System.Collections.Generic; +using osu.Game.Beatmaps.Timing; +using OpenTK; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Objects; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseManiaPlayfield : TestCase + { + public override string Description => @"Mania playfield"; + + protected override double TimePerAction => 200; + + public override void Reset() + { + base.Reset(); + + Action createPlayfield = (cols, pos) => + { + Clear(); + Add(new ManiaPlayfield(cols, new List()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + SpecialColumnPosition = pos, + Scale = new Vector2(1, -1) + }); + }; + + Action createPlayfieldWithNotes = (cols, pos) => + { + Clear(); + + ManiaPlayfield playField; + Add(playField = new ManiaPlayfield(cols, new List { new ControlPoint { BeatLength = 200 } }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + SpecialColumnPosition = pos, + Scale = new Vector2(1, -1) + }); + + for (int i = 0; i < cols; i++) + { + playField.Add(new DrawableNote(new Note + { + StartTime = Time.Current + 1000, + Column = i + })); + } + }; + + AddStep("1 column", () => createPlayfield(1, SpecialColumnPosition.Normal)); + AddStep("4 columns", () => createPlayfield(4, SpecialColumnPosition.Normal)); + AddStep("Left special style", () => createPlayfield(4, SpecialColumnPosition.Left)); + AddStep("Right special style", () => createPlayfield(4, SpecialColumnPosition.Right)); + AddStep("5 columns", () => createPlayfield(5, SpecialColumnPosition.Normal)); + AddStep("8 columns", () => createPlayfield(8, SpecialColumnPosition.Normal)); + AddStep("Left special style", () => createPlayfield(8, SpecialColumnPosition.Left)); + AddStep("Right special style", () => createPlayfield(8, SpecialColumnPosition.Right)); + + AddStep("Normal special style", () => createPlayfield(4, SpecialColumnPosition.Normal)); + + AddStep("Notes", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Normal)); + AddWaitStep(10); + AddStep("Left special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Left)); + AddWaitStep(10); + AddStep("Right special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Right)); + AddWaitStep(10); + } + + private void triggerKeyDown(Column column) + { + column.TriggerKeyDown(new InputState(), new KeyDownEventArgs + { + Key = column.Key, + Repeat = false + }); + } + + private void triggerKeyUp(Column column) + { + column.TriggerKeyUp(new InputState(), new KeyUpEventArgs + { + Key = column.Key + }); + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseMenuOverlays.cs b/osu.Desktop.VisualTests/Tests/TestCaseMenuOverlays.cs index acf98ea86b..23fe8f16db 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseMenuOverlays.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseMenuOverlays.cs @@ -12,7 +12,7 @@ namespace osu.Desktop.VisualTests.Tests { public override string Description => @"Tests pause and fail overlays"; - private PauseOverlay pauseOverlay; + private PauseContainer.PauseOverlay pauseOverlay; private FailOverlay failOverlay; private int retryCount; @@ -22,7 +22,7 @@ namespace osu.Desktop.VisualTests.Tests retryCount = 0; - Add(pauseOverlay = new PauseOverlay + Add(pauseOverlay = new PauseContainer.PauseOverlay { OnResume = () => Logger.Log(@"Resume"), OnRetry = () => Logger.Log(@"Retry"), diff --git a/osu.Desktop.VisualTests/Tests/TestCaseModSelectOverlay.cs b/osu.Desktop.VisualTests/Tests/TestCaseMods.cs similarity index 65% rename from osu.Desktop.VisualTests/Tests/TestCaseModSelectOverlay.cs rename to osu.Desktop.VisualTests/Tests/TestCaseMods.cs index d1c137191f..3f3a9d82f5 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseModSelectOverlay.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseMods.cs @@ -6,16 +6,21 @@ using osu.Framework.Graphics; using osu.Game.Overlays.Mods; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Screens.Play.HUD; +using OpenTK; namespace osu.Desktop.VisualTests.Tests { - internal class TestCaseModSelectOverlay : TestCase + internal class TestCaseMods : TestCase { - public override string Description => @"Tests the mod select overlay"; + public override string Description => @"Mod select overlay and in-game display"; private ModSelectOverlay modSelect; + private ModDisplay modDisplay; + private RulesetDatabase rulesets; + [BackgroundDependencyLoader] private void load(RulesetDatabase rulesets) { @@ -33,6 +38,16 @@ namespace osu.Desktop.VisualTests.Tests Anchor = Anchor.BottomCentre, }); + Add(modDisplay = new ModDisplay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Position = new Vector2(0, 25), + }); + + modDisplay.Current.BindTo(modSelect.SelectedMods); + AddStep("Toggle", modSelect.ToggleVisibility); foreach (var ruleset in rulesets.AllRulesets) diff --git a/osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs b/osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs new file mode 100644 index 0000000000..3cefb8a3d2 --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Testing; +using osu.Game.Overlays; + +namespace osu.Desktop.VisualTests.Tests +{ + internal class TestCaseOnScreenDisplay : TestCase + { + private FrameworkConfigManager config; + private Bindable frameSyncMode; + + public override string Description => @"Make it easier to see setting changes"; + + public override void Reset() + { + base.Reset(); + + Add(new OnScreenDisplay()); + + frameSyncMode = config.GetBindable(FrameworkSetting.FrameSync); + + FrameSync initial = frameSyncMode.Value; + + AddRepeatStep(@"Change frame limiter", setNextMode, 3); + + AddStep(@"Restore frame limiter", () => frameSyncMode.Value = initial); + } + + private void setNextMode() + { + var nextMode = frameSyncMode.Value + 1; + if (nextMode > FrameSync.Unlimited) + nextMode = FrameSync.VSync; + frameSyncMode.Value = nextMode; + } + + [BackgroundDependencyLoader] + private void load(FrameworkConfigManager config) + { + this.config = config; + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseResults.cs b/osu.Desktop.VisualTests/Tests/TestCaseResults.cs index aa3a117667..f8c93e9a73 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseResults.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseResults.cs @@ -47,7 +47,7 @@ namespace osu.Desktop.VisualTests.Tests Accuracy = 0.98, MaxCombo = 123, Rank = ScoreRank.A, - Date = DateTime.Now, + Date = DateTimeOffset.Now, Statistics = new Dictionary() { { "300", 50 }, diff --git a/osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs b/osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs index d8dac63980..f86fa4dab5 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs @@ -3,12 +3,11 @@ using OpenTK; using osu.Framework.Graphics; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Framework.MathUtils; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; -using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play.HUD; namespace osu.Desktop.VisualTests.Tests { diff --git a/osu.Desktop.VisualTests/Tests/TestCaseOptions.cs b/osu.Desktop.VisualTests/Tests/TestCaseSettings.cs similarity index 54% rename from osu.Desktop.VisualTests/Tests/TestCaseOptions.cs rename to osu.Desktop.VisualTests/Tests/TestCaseSettings.cs index ff6bdc8a5a..660085e558 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseOptions.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseSettings.cs @@ -6,18 +6,18 @@ using osu.Game.Overlays; namespace osu.Desktop.VisualTests.Tests { - internal class TestCaseOptions : TestCase + internal class TestCaseSettings : TestCase { - public override string Description => @"Tests the options overlay"; + public override string Description => @"Tests the settings overlay"; - private OptionsOverlay options; + private SettingsOverlay settings; public override void Reset() { base.Reset(); - Children = new[] { options = new OptionsOverlay() }; - options.ToggleVisibility(); + Children = new[] { settings = new SettingsOverlay() }; + settings.ToggleVisibility(); } } } diff --git a/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs b/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs index 6d8aac1d09..e3c343f5f8 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs @@ -18,10 +18,14 @@ namespace osu.Desktop.VisualTests.Tests private SongProgress progress; private SongProgressGraph graph; + private StopwatchClock clock; + public override void Reset() { base.Reset(); + clock = new StopwatchClock(true); + Add(progress = new SongProgress { RelativeSizeAxes = Axes.X, @@ -55,6 +59,9 @@ namespace osu.Desktop.VisualTests.Tests progress.Objects = objects; graph.Objects = objects; + + progress.AudioClock = clock; + progress.OnSeek = pos => clock.Seek(pos); } } } diff --git a/osu.Desktop.VisualTests/Tests/TestCaseTabControl.cs b/osu.Desktop.VisualTests/Tests/TestCaseTabControl.cs index b72abd1992..96933a15e7 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseTabControl.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseTabControl.cs @@ -1,8 +1,8 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Graphics; using OpenTK; -using osu.Framework.Graphics.Primitives; using osu.Framework.Testing; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; diff --git a/osu.Desktop.VisualTests/Tests/TestCaseTwoLayerButton.cs b/osu.Desktop.VisualTests/Tests/TestCaseTwoLayerButton.cs index 2427b6d12c..ba17cfc3d8 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseTwoLayerButton.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseTwoLayerButton.cs @@ -16,7 +16,7 @@ namespace osu.Desktop.VisualTests.Tests base.Reset(); Add(new BackButton()); - Add(new SkipButton()); + Add(new SkipButton(Clock.CurrentTime + 5000)); } } } diff --git a/osu.Desktop.VisualTests/VisualTestGame.cs b/osu.Desktop.VisualTests/VisualTestGame.cs index e0d168390b..5c5bcd9e21 100644 --- a/osu.Desktop.VisualTests/VisualTestGame.cs +++ b/osu.Desktop.VisualTests/VisualTestGame.cs @@ -29,7 +29,7 @@ namespace osu.Desktop.VisualTests host.DrawThread.InactiveHz = host.DrawThread.ActiveHz; host.InputThread.InactiveHz = host.InputThread.ActiveHz; - host.Window.CursorState = CursorState.Hidden; + host.Window.CursorState |= CursorState.Hidden; } } } diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index a2228ca9aa..2374b07bf3 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -190,9 +190,12 @@ + + + @@ -209,9 +212,9 @@ - + - + diff --git a/osu.Desktop/Beatmaps/IO/LegacyFilesystemReader.cs b/osu.Desktop/Beatmaps/IO/LegacyFilesystemReader.cs index abc45d82ec..8c896646bf 100644 --- a/osu.Desktop/Beatmaps/IO/LegacyFilesystemReader.cs +++ b/osu.Desktop/Beatmaps/IO/LegacyFilesystemReader.cs @@ -14,7 +14,7 @@ namespace osu.Desktop.Beatmaps.IO { public static void Register() => AddReader((storage, path) => Directory.Exists(path)); - private string basePath { get; } + private readonly string basePath; public LegacyFilesystemReader(string path) { diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index c2bb39ac4a..299f64d998 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -43,7 +43,7 @@ namespace osu.Desktop var desktopWindow = host.Window as DesktopGameWindow; if (desktopWindow != null) { - desktopWindow.CursorState = CursorState.Hidden; + desktopWindow.CursorState |= CursorState.Hidden; desktopWindow.Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location); desktopWindow.Title = Name; diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index a6faf13d51..53449fd5f5 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch { public class CatchRuleset : Ruleset { - public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap) => new CatchHitRenderer(beatmap); + public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new CatchHitRenderer(beatmap, isForCurrentRuleset); public override IEnumerable GetModsFor(ModType type) { diff --git a/osu.Game.Rulesets.Catch/UI/CatchHitRenderer.cs b/osu.Game.Rulesets.Catch/UI/CatchHitRenderer.cs index f34585be55..179440adb3 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchHitRenderer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchHitRenderer.cs @@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Catch.UI { public class CatchHitRenderer : HitRenderer { - public CatchHitRenderer(WorkingBeatmap beatmap) - : base(beatmap) + public CatchHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(beatmap, isForCurrentRuleset) { } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 847af965cc..933fe0787c 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -1,23 +1,153 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.Objects; -using System.Collections.Generic; -using System; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using System; +using System.Collections.Generic; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Mania.Beatmaps.Patterns; +using osu.Game.Rulesets.Mania.MathUtils; +using osu.Game.Database; namespace osu.Game.Rulesets.Mania.Beatmaps { - internal class ManiaBeatmapConverter : BeatmapConverter + public class ManiaBeatmapConverter : BeatmapConverter { protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; - protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) + private Pattern lastPattern = new Pattern(); + private FastRandom random; + private Beatmap beatmap; + private bool isForCurrentRuleset; + + protected override Beatmap ConvertBeatmap(Beatmap original, bool isForCurrentRuleset) { - yield return null; + this.isForCurrentRuleset = isForCurrentRuleset; + + beatmap = original; + + BeatmapDifficulty difficulty = original.BeatmapInfo.Difficulty; + + int seed = (int)Math.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)Math.Round(difficulty.ApproachRate); + random = new FastRandom(seed); + + return base.ConvertBeatmap(original, isForCurrentRuleset); + } + + protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) + { + var maniaOriginal = original as ManiaHitObject; + if (maniaOriginal != null) + { + yield return maniaOriginal; + yield break; + } + + var objects = isForCurrentRuleset ? generateSpecific(original) : generateConverted(original); + + if (objects == null) + yield break; + + foreach (ManiaHitObject obj in objects) + yield return obj; + } + + /// + /// Method that generates hit objects for osu!mania specific beatmaps. + /// + /// The original hit object. + /// The hit objects generated. + private IEnumerable generateSpecific(HitObject original) + { + var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern); + + Pattern newPattern = generator.Generate(); + lastPattern = newPattern; + + return newPattern.HitObjects; + } + + /// + /// Method that generates hit objects for non-osu!mania beatmaps. + /// + /// The original hit object. + /// The hit objects generated. + private IEnumerable generateConverted(HitObject original) + { + var endTimeData = original as IHasEndTime; + var distanceData = original as IHasDistance; + var positionData = original as IHasPosition; + + // Following lines currently commented out to appease resharper + + //Patterns.PatternGenerator conversion = null; + + if (distanceData != null) + { + // Slider + } + else if (endTimeData != null) + { + // Spinner + } + else if (positionData != null) + { + // Circle + } + + //if (conversion == null) + return null; + + //Pattern newPattern = conversion.Generate(); + //lastPattern = newPattern; + + //return newPattern.HitObjects; + } + + /// + /// A pattern generator for osu!mania-specific beatmaps. + /// + private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator + { + public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern) + : base(random, hitObject, beatmap, previousPattern) + { + } + + public override Pattern Generate() + { + var endTimeData = HitObject as IHasEndTime; + var positionData = HitObject as IHasXPosition; + + int column = GetColumn(positionData?.X ?? 0); + + var pattern = new Pattern(); + + if (endTimeData != null) + { + pattern.Add(new HoldNote + { + StartTime = HitObject.StartTime, + Samples = HitObject.Samples, + Duration = endTimeData.Duration, + Column = column, + }); + } + else if (positionData != null) + { + pattern.Add(new Note + { + StartTime = HitObject.StartTime, + Samples = HitObject.Samples, + Column = column + }); + } + + return pattern; + } } } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs new file mode 100644 index 0000000000..ad07c03b96 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -0,0 +1,106 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Rulesets.Mania.MathUtils; +using osu.Game.Rulesets.Objects; +using OpenTK; + +namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy +{ + /// + /// A pattern generator for legacy hit objects. + /// + internal abstract class PatternGenerator : Patterns.PatternGenerator + { + /// + /// The column index at which to start generating random notes. + /// + protected readonly int RandomStart; + + /// + /// The random number generator to use. + /// + protected readonly FastRandom Random; + + protected PatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern) + : base(hitObject, beatmap, previousPattern) + { + Random = random; + + RandomStart = AvailableColumns == 8 ? 1 : 0; + } + + /// + /// Converts an x-position into a column. + /// + /// The x-position. + /// Whether to treat as 7K + 1. + /// The column. + protected int GetColumn(float position, bool allowSpecial = false) + { + if (allowSpecial && AvailableColumns == 8) + { + const float local_x_divisor = 512f / 7; + return MathHelper.Clamp((int)Math.Floor(position / local_x_divisor), 0, 6) + 1; + } + + float localXDivisor = 512f / AvailableColumns; + return MathHelper.Clamp((int)Math.Floor(position / localXDivisor), 0, AvailableColumns - 1); + } + + /// + /// Generates a count of notes to be generated from probabilities. + /// + /// Probability for 2 notes to be generated. + /// Probability for 3 notes to be generated. + /// Probability for 4 notes to be generated. + /// Probability for 5 notes to be generated. + /// Probability for 6 notes to be generated. + /// The amount of notes to be generated. + protected int GetRandomNoteCount(double p2, double p3, double p4 = 0, double p5 = 0, double p6 = 0) + { + double val = Random.NextDouble(); + if (val >= 1 - p6) + return 6; + if (val >= 1 - p5) + return 5; + if (val >= 1 - p4) + return 4; + if (val >= 1 - p3) + return 3; + return val >= 1 - p2 ? 2 : 1; + } + + private double? conversionDifficulty; + /// + /// A difficulty factor used for various conversion methods from osu!stable. + /// + protected double ConversionDifficulty + { + get + { + if (conversionDifficulty != null) + return conversionDifficulty.Value; + + HitObject lastObject = Beatmap.HitObjects.LastOrDefault(); + HitObject firstObject = Beatmap.HitObjects.FirstOrDefault(); + + double drainTime = (lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0); + drainTime -= Beatmap.EventInfo.TotalBreakTime; + + if (drainTime == 0) + drainTime = 10000; + + BeatmapDifficulty difficulty = Beatmap.BeatmapInfo.Difficulty; + conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; + conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); + + return conversionDifficulty.Value; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs new file mode 100644 index 0000000000..d4957d41a9 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternType.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; + +namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy +{ + /// + /// The type of pattern to generate. Used for legacy patterns. + /// + [Flags] + internal enum PatternType + { + None = 0, + /// + /// Keep the same as last row. + /// + ForceStack = 1, + /// + /// Keep different from last row. + /// + ForceNotStack = 2, + /// + /// Keep as single note at its original position. + /// + KeepSingle = 4, + /// + /// Use a lower random value. + /// + LowProbability = 8, + /// + /// Reserved. + /// + Alternate = 16, + /// + /// Ignore the repeat count. + /// + ForceSigSlider = 32, + /// + /// Convert slider to circle. + /// + ForceNotSlider = 64, + /// + /// Notes gathered together. + /// + Gathered = 128, + Mirror = 256, + /// + /// Change 0 -> 6. + /// + Reverse = 512, + /// + /// 1 -> 5 -> 1 -> 5 like reverse. + /// + Cycle = 1024, + /// + /// Next note will be at column + 1. + /// + Stair = 2048, + /// + /// Next note will be at column - 1. + /// + ReverseStair = 4096 + } +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs new file mode 100644 index 0000000000..cbde1f0f53 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Mania.Objects; + +namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns +{ + /// + /// Creates a pattern containing hit objects. + /// + internal class Pattern + { + private readonly List hitObjects = new List(); + + /// + /// All the hit objects contained in this pattern. + /// + public IEnumerable HitObjects => hitObjects; + + /// + /// Whether this pattern already contains a hit object in a code. + /// + /// The column index. + /// Whether this pattern already contains a hit object in + public bool IsFilled(int column) => hitObjects.Exists(h => h.Column == column); + + /// + /// Amount of columns taken up by hit objects in this pattern. + /// + public int ColumnsFilled => HitObjects.GroupBy(h => h.Column).Count(); + + /// + /// Adds a hit object to this pattern. + /// + /// The hit object to add. + public void Add(ManiaHitObject hitObject) => hitObjects.Add(hitObject); + + /// + /// Copies hit object from another pattern to this one. + /// + /// The other pattern. + public void Add(Pattern other) + { + other.HitObjects.ForEach(Add); + } + + /// + /// Clears this pattern, removing all hit objects. + /// + public void Clear() => hitObjects.Clear(); + + /// + /// Removes a hit object from this pattern. + /// + /// The hit object to remove. + public bool Remove(ManiaHitObject hitObject) => hitObjects.Remove(hitObject); + } +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs new file mode 100644 index 0000000000..dda4d07182 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2007-2017 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; + +namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns +{ + /// + /// Generator to create a pattern from a hit object. + /// + internal abstract class PatternGenerator + { + /// + /// The number of columns available to create the pattern. + /// + protected readonly int AvailableColumns; + + /// + /// The last pattern. + /// + protected readonly Pattern PreviousPattern; + + /// + /// The hit object to create the pattern for. + /// + protected readonly HitObject HitObject; + + /// + /// The beatmap which is a part of. + /// + protected readonly Beatmap Beatmap; + + protected PatternGenerator(HitObject hitObject, Beatmap beatmap, Pattern previousPattern) + { + PreviousPattern = previousPattern; + HitObject = hitObject; + Beatmap = beatmap; + + AvailableColumns = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.CircleSize); + } + + /// + /// Generates the pattern for , filled with hit objects. + /// + /// The containing the hit objects. + public abstract Pattern Generate(); + } +} diff --git a/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs b/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs new file mode 100644 index 0000000000..2a0ce88506 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs @@ -0,0 +1,179 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Database; + +namespace osu.Game.Rulesets.Mania.Judgements +{ + public class HitWindows + { + #region Constants + + /// + /// PERFECT hit window at OD = 10. + /// + private const double perfect_min = 27.8; + /// + /// PERFECT hit window at OD = 5. + /// + private const double perfect_mid = 38.8; + /// + /// PERFECT hit window at OD = 0. + /// + private const double perfect_max = 44.8; + + /// + /// GREAT hit window at OD = 10. + /// + private const double great_min = 68; + /// + /// GREAT hit window at OD = 5. + /// + private const double great_mid = 98; + /// + /// GREAT hit window at OD = 0. + /// + private const double great_max = 128; + + /// + /// GOOD hit window at OD = 10. + /// + private const double good_min = 134; + /// + /// GOOD hit window at OD = 5. + /// + private const double good_mid = 164; + /// + /// GOOD hit window at OD = 0. + /// + private const double good_max = 194; + + /// + /// OK hit window at OD = 10. + /// + private const double ok_min = 194; + /// + /// OK hit window at OD = 5. + /// + private const double ok_mid = 224; + /// + /// OK hit window at OD = 0. + /// + private const double ok_max = 254; + + /// + /// BAD hit window at OD = 10. + /// + private const double bad_min = 242; + /// + /// BAD hit window at OD = 5. + /// + private const double bad_mid = 272; + /// + /// BAD hit window at OD = 0. + /// + private const double bad_max = 302; + + /// + /// MISS hit window at OD = 10. + /// + private const double miss_min = 316; + /// + /// MISS hit window at OD = 5. + /// + private const double miss_mid = 346; + /// + /// MISS hit window at OD = 0. + /// + private const double miss_max = 376; + + #endregion + + /// + /// Hit window for a PERFECT hit. + /// + public double Perfect = perfect_mid; + + /// + /// Hit window for a GREAT hit. + /// + public double Great = great_mid; + + /// + /// Hit window for a GOOD hit. + /// + public double Good = good_mid; + + /// + /// Hit window for an OK hit. + /// + public double Ok = ok_mid; + + /// + /// Hit window for a BAD hit. + /// + public double Bad = bad_mid; + + /// + /// Hit window for a MISS hit. + /// + public double Miss = miss_mid; + + /// + /// Constructs default hit windows. + /// + public HitWindows() + { + } + + /// + /// Constructs hit windows by fitting a parameter to a 2-part piecewise linear function for each hit window. + /// + /// The parameter. + public HitWindows(double difficulty) + { + Perfect = BeatmapDifficulty.DifficultyRange(difficulty, perfect_max, perfect_mid, perfect_min); + Great = BeatmapDifficulty.DifficultyRange(difficulty, great_max, great_mid, great_min); + Good = BeatmapDifficulty.DifficultyRange(difficulty, good_max, good_mid, good_min); + Ok = BeatmapDifficulty.DifficultyRange(difficulty, ok_max, ok_mid, ok_min); + Bad = BeatmapDifficulty.DifficultyRange(difficulty, bad_max, bad_mid, bad_min); + Miss = BeatmapDifficulty.DifficultyRange(difficulty, miss_max, miss_mid, miss_min); + } + + /// + /// Constructs new hit windows which have been multiplied by a value. + /// + /// The original hit windows. + /// The value to multiply each hit window by. + public static HitWindows operator *(HitWindows windows, double value) + { + return new HitWindows + { + Perfect = windows.Perfect * value, + Great = windows.Great * value, + Good = windows.Good * value, + Ok = windows.Ok * value, + Bad = windows.Bad * value, + Miss = windows.Miss * value + }; + } + + /// + /// Constructs new hit windows which have been divided by a value. + /// + /// The original hit windows. + /// The value to divide each hit window by. + public static HitWindows operator /(HitWindows windows, double value) + { + return new HitWindows + { + Perfect = windows.Perfect / value, + Great = windows.Great / value, + Good = windows.Good / value, + Ok = windows.Ok / value, + Bad = windows.Bad / value, + Miss = windows.Miss / value + }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index e9bcc60d2c..aaba4d94f0 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -9,7 +9,7 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.Mania { - public class ManiaDifficultyCalculator : DifficultyCalculator + public class ManiaDifficultyCalculator : DifficultyCalculator { public ManiaDifficultyCalculator(Beatmap beatmap) : base(beatmap) @@ -21,6 +21,6 @@ namespace osu.Game.Rulesets.Mania return 0; } - protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 26614075b1..30d1846746 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania { public class ManiaRuleset : Ruleset { - public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap) => new ManiaHitRenderer(beatmap); + public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new ManiaHitRenderer(beatmap, isForCurrentRuleset); public override IEnumerable GetModsFor(ModType type) { diff --git a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs new file mode 100644 index 0000000000..ff3fd8e4b7 --- /dev/null +++ b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs @@ -0,0 +1,92 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; + +namespace osu.Game.Rulesets.Mania.MathUtils +{ + /// + /// A PRNG specified in http://heliosphan.org/fastrandom.html. + /// + internal class FastRandom + { + private const double uint_to_real = 1.0 / (uint.MaxValue + 1.0); + private const uint int_mask = 0x7FFFFFFF; + private const uint y = 842502087; + private const uint z = 3579807591; + private const uint w = 273326509; + private uint _x, _y = y, _z = z, _w = w; + + public FastRandom(int seed) + { + _x = (uint)seed; + } + + public FastRandom() + : this(Environment.TickCount) + { + } + + /// + /// Generates a random unsigned integer within the range [, ). + /// + /// The random value. + public uint NextUInt() + { + uint t = _x ^ _x << 11; + _x = _y; + _y = _z; + _z = _w; + return _w = _w ^ _w >> 19 ^ t ^ t >> 8; + } + + /// + /// Generates a random integer value within the range [0, ). + /// + /// The random value. + public int Next() => (int)(int_mask & NextUInt()); + + /// + /// Generates a random integer value within the range [0, ). + /// + /// The upper bound. + /// The random value. + public int Next(int upperBound) => (int)(NextDouble() * upperBound); + + /// + /// Generates a random integer value within the range [, ). + /// + /// The lower bound of the range. + /// The upper bound of the range. + /// The random value. + public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound)); + + /// + /// Generates a random double value within the range [0, 1). + /// + /// The random value. + public double NextDouble() => uint_to_real * NextUInt(); + + private uint bitBuffer; + private int bitIndex = 32; + + /// + /// Generates a reandom boolean value. Cached such that a random value is only generated once in every 32 calls. + /// + /// The random value. + public bool NextBool() + { + if (bitIndex == 32) + { + bitBuffer = NextUInt(); + bitIndex = 1; + + return (bitBuffer & 1) == 1; + } + + bitIndex++; + return ((bitBuffer >>= 1) & 1) == 1; + } + + } +} diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs index 68458caeac..b402d3a010 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs @@ -64,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public override string Name => "FadeIn"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_hidden; + public override ModType Type => ModType.DifficultyIncrease; public override double ScoreMultiplier => 1; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawable/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawable/DrawableNote.cs deleted file mode 100644 index 07a27b1643..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/Drawable/DrawableNote.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Graphics; -using OpenTK; - -namespace osu.Game.Rulesets.Mania.Objects.Drawable -{ - public class DrawableNote : Sprite - { - private readonly ManiaBaseHit note; - - public DrawableNote(ManiaBaseHit note) - { - this.note = note; - Origin = Anchor.Centre; - Scale = new Vector2(0.1f); - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - Texture = textures.Get(@"Menu/logo"); - - const double duration = 0; - - Transforms.Add(new TransformPositionY { StartTime = note.StartTime - 200, EndTime = note.StartTime, StartValue = -0.1f, EndValue = 0.9f }); - Transforms.Add(new TransformAlpha { StartTime = note.StartTime + duration + 200, EndTime = note.StartTime + duration + 400, StartValue = 1, EndValue = 0 }); - Expire(true); - } - } -} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs new file mode 100644 index 0000000000..d9e46f4720 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -0,0 +1,76 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + public class DrawableHoldNote : DrawableManiaHitObject + { + private readonly NotePiece headPiece; + private readonly BodyPiece bodyPiece; + private readonly NotePiece tailPiece; + + public DrawableHoldNote(HoldNote hitObject) + : base(hitObject) + { + RelativeSizeAxes = Axes.Both; + Height = (float)HitObject.Duration; + + Add(new Drawable[] + { + // For now the body piece covers the entire height of the container + // whereas possibly in the future we don't want to extend under the head/tail. + // This will be fixed when new designs are given or the current design is finalized. + bodyPiece = new BodyPiece + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + headPiece = new NotePiece + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + tailPiece = new NotePiece + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.TopCentre + } + }); + } + + public override Color4 AccentColour + { + get { return base.AccentColour; } + set + { + if (base.AccentColour == value) + return; + base.AccentColour = value; + + headPiece.AccentColour = value; + bodyPiece.AccentColour = value; + tailPiece.AccentColour = value; + } + } + + protected override void UpdateState(ArmedState state) + { + } + + protected override void Update() + { + if (Time.Current > HitObject.StartTime) + headPiece.Colour = Color4.Green; + if (Time.Current > HitObject.EndTime) + { + bodyPiece.Colour = Color4.Green; + tailPiece.Colour = Color4.Green; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs new file mode 100644 index 0000000000..d33a8c48ee --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2017 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.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + public abstract class DrawableManiaHitObject : DrawableHitObject + where TObject : ManiaHitObject + { + public new TObject HitObject; + + protected DrawableManiaHitObject(TObject hitObject) + : base(hitObject) + { + HitObject = hitObject; + + RelativePositionAxes = Axes.Y; + Y = (float)HitObject.StartTime; + } + + public override Color4 AccentColour + { + get { return base.AccentColour; } + set + { + if (base.AccentColour == value) + return; + base.AccentColour = value; + } + } + + protected override ManiaJudgement CreateJudgement() => new ManiaJudgement(); + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs new file mode 100644 index 0000000000..b216c362f5 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2007-2017 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.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + public class DrawableNote : DrawableManiaHitObject + { + private readonly NotePiece headPiece; + + public DrawableNote(Note hitObject) + : base(hitObject) + { + RelativeSizeAxes = Axes.Both; + Height = 100; + + Add(headPiece = new NotePiece + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }); + } + + public override Color4 AccentColour + { + get { return base.AccentColour; } + set + { + if (base.AccentColour == value) + return; + base.AccentColour = value; + + headPiece.AccentColour = value; + } + } + + protected override void Update() + { + if (Time.Current > HitObject.StartTime) + Colour = Color4.Green; + } + + protected override void UpdateState(ArmedState state) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs new file mode 100644 index 0000000000..c10aa9994b --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2007-2017 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.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces +{ + /// + /// Represents length-wise portion of a hold note. + /// + internal class BodyPiece : Container, IHasAccentColour + { + private readonly Box box; + + public BodyPiece() + { + RelativeSizeAxes = Axes.Both; + + Children = new[] + { + box = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.3f + } + }; + } + + private Color4 accentColour; + public Color4 AccentColour + { + get { return accentColour; } + set + { + if (accentColour == value) + return; + accentColour = value; + + box.Colour = accentColour; + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs new file mode 100644 index 0000000000..e01199e929 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces +{ + /// + /// Represents the static hit markers of notes. + /// + internal class NotePiece : Container, IHasAccentColour + { + private const float head_height = 10; + private const float head_colour_height = 6; + + private readonly Box colouredBox; + + public NotePiece() + { + RelativeSizeAxes = Axes.X; + Height = head_height; + + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both + }, + colouredBox = new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Height = head_colour_height, + Alpha = 0.2f + } + }; + } + + private Color4 accentColour; + public Color4 AccentColour + { + get { return accentColour; } + set + { + if (accentColour == value) + return; + accentColour = value; + + colouredBox.Colour = AccentColour.Lighten(0.9f); + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index e8ce1da77f..a25b8fbf2a 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -1,9 +1,37 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps.Timing; +using osu.Game.Database; +using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Rulesets.Objects.Types; + namespace osu.Game.Rulesets.Mania.Objects { - public class HoldNote : Note + /// + /// Represents a hit object which requires pressing, holding, and releasing a key. + /// + public class HoldNote : Note, IHasEndTime { + /// + /// Lenience of release hit windows. This is to make cases where the hold note release + /// is timed alongside presses of other hit objects less awkward. + /// + private const double release_window_lenience = 1.5; + + public double Duration { get; set; } + public double EndTime => StartTime + Duration; + + /// + /// The key-release hit windows for this hold note. + /// + protected HitWindows ReleaseHitWindows = new HitWindows(); + + public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty) + { + base.ApplyDefaults(timing, difficulty); + + ReleaseHitWindows = HitWindows * release_window_lenience; + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaBaseHit.cs b/osu.Game.Rulesets.Mania/Objects/ManiaBaseHit.cs deleted file mode 100644 index 4c15b69eb7..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/ManiaBaseHit.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Rulesets.Objects; - -namespace osu.Game.Rulesets.Mania.Objects -{ - public abstract class ManiaBaseHit : HitObject - { - public int Column; - } -} diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs new file mode 100644 index 0000000000..f6eb4aea2c --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Mania.Objects.Types; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Mania.Objects +{ + public abstract class ManiaHitObject : HitObject, IHasColumn + { + public int Column { get; set; } + + /// + /// The number of other that start at + /// the same time as this hit object. + /// + public int Siblings { get; set; } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 5a6d6003db..1d2e4169b5 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.cs @@ -1,9 +1,27 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps.Timing; +using osu.Game.Database; +using osu.Game.Rulesets.Mania.Judgements; + namespace osu.Game.Rulesets.Mania.Objects { - public class Note : ManiaBaseHit + /// + /// Represents a hit object which has a single hit press. + /// + public class Note : ManiaHitObject { + /// + /// The key-press hit window for this note. + /// + protected HitWindows HitWindows = new HitWindows(); + + public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty) + { + base.ApplyDefaults(timing, difficulty); + + HitWindows = new HitWindows(difficulty.OverallDifficulty); + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Types/IHasColumn.cs b/osu.Game.Rulesets.Mania/Objects/Types/IHasColumn.cs new file mode 100644 index 0000000000..8281d0d9e4 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Types/IHasColumn.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Mania.Objects.Types +{ + /// + /// A type of hit object which lies in one of a number of predetermined columns. + /// + public interface IHasColumn + { + /// + /// The column which the hit object lies in. + /// + int Column { get; } + } +} diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index ba0304a44a..96f04f79d4 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -8,13 +8,13 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Scoring { - internal class ManiaScoreProcessor : ScoreProcessor + internal class ManiaScoreProcessor : ScoreProcessor { public ManiaScoreProcessor() { } - public ManiaScoreProcessor(HitRenderer hitRenderer) + public ManiaScoreProcessor(HitRenderer hitRenderer) : base(hitRenderer) { } diff --git a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs new file mode 100644 index 0000000000..6c39ba40f9 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs @@ -0,0 +1,153 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using OpenTK; +using osu.Game.Beatmaps.Timing; + +namespace osu.Game.Rulesets.Mania.Timing +{ + /// + /// A container in which added drawables are put into a relative coordinate space spanned by a length of time. + /// + /// This container contains s which scroll inside this container. + /// Drawables added to this container are moved inside the relevant , + /// and as such, will scroll along with the s. + /// + /// + public class ControlPointContainer : Container + { + /// + /// The amount of time which this container spans. + /// + public double TimeSpan { get; set; } + + private readonly List drawableControlPoints; + + public ControlPointContainer(IEnumerable timingChanges) + { + drawableControlPoints = timingChanges.Select(t => new DrawableControlPoint(t)).ToList(); + Children = drawableControlPoints; + } + + /// + /// Adds a drawable to this container. Note that the drawable added must have its Y-position be + /// an absolute unit of time that is _not_ relative to . + /// + /// The drawable to add. + public override void Add(Drawable drawable) + { + // Always add timing sections to ourselves + if (drawable is DrawableControlPoint) + { + base.Add(drawable); + return; + } + + var controlPoint = drawableControlPoints.LastOrDefault(t => t.CanContain(drawable)) ?? drawableControlPoints.FirstOrDefault(); + + if (controlPoint == null) + throw new Exception("Could not find suitable timing section to add object to."); + + controlPoint.Add(drawable); + } + + /// + /// A container that contains drawables within the time span of a timing section. + /// + /// The content of this container will scroll relative to the current time. + /// + /// + private class DrawableControlPoint : Container + { + private readonly ControlPoint timingChange; + + protected override Container Content => content; + private readonly Container content; + + /// + /// Creates a drawable control point. The height of this container will be proportional + /// to the beat length of the control point it is initialized with such that, e.g. a beat length + /// of 500ms results in this container being twice as high as its parent, which further means that + /// the content container will scroll at twice the normal rate. + /// + /// The control point to create the drawable control point for. + public DrawableControlPoint(ControlPoint timingChange) + { + this.timingChange = timingChange; + + RelativeSizeAxes = Axes.Both; + + AddInternal(content = new AutoTimeRelativeContainer + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Y = (float)timingChange.Time + }); + } + + protected override void Update() + { + var parent = (ControlPointContainer)Parent; + + // Adjust our height to account for the speed changes + Height = (float)(1000 / timingChange.BeatLength / timingChange.SpeedMultiplier); + RelativeCoordinateSpace = new Vector2(1, (float)parent.TimeSpan); + + // Scroll the content + content.Y = (float)(timingChange.Time - Time.Current); + } + + public override void Add(Drawable drawable) + { + // The previously relatively-positioned drawable will now become relative to content, but since the drawable has no knowledge of content, + // we need to offset it back by content's position position so that it becomes correctly relatively-positioned to content + // This can be removed if hit objects were stored such that either their StartTime or their "beat offset" was relative to the timing change + // they belonged to, but this requires a radical change to the beatmap format which we're not ready to do just yet + drawable.Y -= (float)timingChange.Time; + + base.Add(drawable); + } + + /// + /// Whether this control point can contain a drawable. This control point can contain a drawable if the drawable is positioned "after" this control point. + /// + /// The drawable to check. + public bool CanContain(Drawable drawable) => content.Y <= drawable.Y; + + /// + /// A container which always keeps its height and relative coordinate space "auto-sized" to its children. + /// + /// This is used in the case where children are relatively positioned/sized to time values (e.g. notes/bar lines) to keep + /// such children wrapped inside a container, otherwise they would disappear due to container flattening. + /// + /// + private class AutoTimeRelativeContainer : Container + { + public override void InvalidateFromChild(Invalidation invalidation) + { + // We only want to re-compute our size when a child's size or position has changed + if ((invalidation & Invalidation.Geometry) == 0) + { + base.InvalidateFromChild(invalidation); + return; + } + + if (!Children.Any()) + return; + + float height = Children.Select(child => child.Y + child.Height).Max(); + + Height = height; + RelativeCoordinateSpace = new Vector2(1, height); + + base.InvalidateFromChild(invalidation); + } + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs new file mode 100644 index 0000000000..dea00433e6 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -0,0 +1,211 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + + +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Input; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Colour; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Timing; +using System.Collections.Generic; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Judgements; +using osu.Game.Beatmaps.Timing; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class Column : Container, IHasAccentColour + { + private const float key_icon_size = 10; + private const float key_icon_corner_radius = 3; + private const float key_icon_border_radius = 2; + + private const float hit_target_height = 10; + private const float hit_target_bar_height = 2; + + private const float column_width = 45; + private const float special_column_width = 70; + + public Key Key; + + private readonly Box background; + private readonly Container hitTargetBar; + private readonly Container keyIcon; + + public readonly ControlPointContainer ControlPointContainer; + + public Column(IEnumerable timingChanges) + { + RelativeSizeAxes = Axes.Y; + Width = column_width; + + Children = new Drawable[] + { + background = new Box + { + Name = "Foreground", + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f + }, + new Container + { + Name = "Hit target + hit objects", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = ManiaPlayfield.HIT_TARGET_POSITION}, + Children = new Drawable[] + { + new Container + { + Name = "Hit target", + RelativeSizeAxes = Axes.X, + Height = hit_target_height, + Children = new Drawable[] + { + new Box + { + Name = "Background", + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + hitTargetBar = new Container + { + Name = "Bar", + RelativeSizeAxes = Axes.X, + Height = hit_target_bar_height, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both + } + } + } + } + }, + ControlPointContainer = new ControlPointContainer(timingChanges) + { + Name = "Hit objects", + RelativeSizeAxes = Axes.Both, + }, + } + }, + new Container + { + Name = "Key", + RelativeSizeAxes = Axes.X, + Height = ManiaPlayfield.HIT_TARGET_POSITION, + Children = new Drawable[] + { + new Box + { + Name = "Key gradient", + RelativeSizeAxes = Axes.Both, + ColourInfo = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)), + Alpha = 0.5f + }, + keyIcon = new Container + { + Name = "Key icon", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(key_icon_size), + Masking = true, + CornerRadius = key_icon_corner_radius, + BorderThickness = 2, + BorderColour = Color4.White, // Not true + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + } + } + } + }; + } + + private bool isSpecial; + public bool IsSpecial + { + get { return isSpecial; } + set + { + if (isSpecial == value) + return; + isSpecial = value; + + Width = isSpecial ? special_column_width : column_width; + } + } + + private Color4 accentColour; + public Color4 AccentColour + { + get { return accentColour; } + set + { + if (accentColour == value) + return; + accentColour = value; + + background.Colour = accentColour; + + hitTargetBar.EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = accentColour.Opacity(0.5f), + }; + + keyIcon.EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = accentColour.Opacity(0.5f), + }; + } + } + + public void Add(DrawableHitObject hitObject) + { + ControlPointContainer.Add(hitObject); + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Repeat) + return false; + + if (args.Key == Key) + { + background.FadeTo(background.Alpha + 0.2f, 50, EasingTypes.OutQuint); + keyIcon.ScaleTo(1.4f, 50, EasingTypes.OutQuint); + } + + return false; + } + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + { + if (args.Key == Key) + { + background.FadeTo(0.2f, 800, EasingTypes.OutQuart); + keyIcon.ScaleTo(1f, 400, EasingTypes.OutQuart); + } + + return false; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs index 7fb8f95b4c..0bf70017e3 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs @@ -1,34 +1,92 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Linq; +using OpenTK; +using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.UI { - public class ManiaHitRenderer : HitRenderer + public class ManiaHitRenderer : HitRenderer { - private readonly int columns; + public int? Columns; - public ManiaHitRenderer(WorkingBeatmap beatmap, int columns = 5) - : base(beatmap) + public ManiaHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(beatmap, isForCurrentRuleset) { - this.columns = columns; + } + + protected override Playfield CreatePlayfield() + { + ControlPoint firstTimingChange = Beatmap.TimingInfo.ControlPoints.FirstOrDefault(t => t.TimingChange); + + if (firstTimingChange == null) + throw new Exception("The Beatmap contains no timing points!"); + + // Generate the timing points, making non-timing changes use the previous timing change + var timingChanges = Beatmap.TimingInfo.ControlPoints.Select(c => + { + ControlPoint t = c.Clone(); + + if (c.TimingChange) + firstTimingChange = c; + else + t.BeatLength = firstTimingChange.BeatLength; + + return t; + }); + + double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; + + // Perform some post processing of the timing changes + timingChanges = timingChanges + // Collapse sections after the last hit object + .Where(s => s.Time <= lastObjectTime) + // Collapse sections with the same start time + .GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time) + // Collapse sections with the same beat length + .GroupBy(s => s.BeatLength * s.SpeedMultiplier).Select(g => g.First()) + .ToList(); + + return new ManiaPlayfield(Columns ?? (int)Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize), timingChanges) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + // Invert by default for now (should be moved to config/skin later) + Scale = new Vector2(1, -1) + }; } public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); - protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); - protected override Playfield CreatePlayfield() => new ManiaPlayfield(columns); + protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) + { + var holdNote = h as HoldNote; + if (holdNote != null) + return new DrawableHoldNote(holdNote); - protected override DrawableHitObject GetVisualRepresentation(ManiaBaseHit h) => null; + var note = h as Note; + if (note != null) + return new DrawableNote(note); + + return null; + } + + protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(1, 0.8f); } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index 5eea3d70c0..56a86873e9 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -8,29 +8,253 @@ using osu.Game.Rulesets.UI; using OpenTK; using OpenTK.Graphics; using osu.Game.Rulesets.Mania.Judgements; +using osu.Framework.Graphics.Containers; +using System; +using osu.Game.Graphics; +using osu.Framework.Allocation; +using OpenTK.Input; +using System.Linq; +using System.Collections.Generic; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Mania.Timing; +using osu.Framework.Input; +using osu.Game.Beatmaps.Timing; +using osu.Framework.Graphics.Transforms; +using osu.Framework.MathUtils; namespace osu.Game.Rulesets.Mania.UI { - public class ManiaPlayfield : Playfield + public class ManiaPlayfield : Playfield { - public ManiaPlayfield(int columns) + public const float HIT_TARGET_POSITION = 50; + + private const float time_span_default = 5000; + private const float time_span_min = 10; + private const float time_span_max = 50000; + private const float time_span_step = 200; + + /// + /// Default column keys, expanding outwards from the middle as more column are added. + /// E.g. 2 columns use FJ, 4 columns use DFJK, 6 use SDFJKL, etc... + /// + private static readonly Key[] default_keys = { Key.A, Key.S, Key.D, Key.F, Key.J, Key.K, Key.L, Key.Semicolon }; + + private SpecialColumnPosition specialColumnPosition; + /// + /// The style to use for the special column. + /// + public SpecialColumnPosition SpecialColumnPosition { - Size = new Vector2(0.8f, 1f); - Anchor = Anchor.BottomCentre; - Origin = Anchor.BottomCentre; + get { return specialColumnPosition; } + set + { + if (IsLoaded) + throw new InvalidOperationException($"Setting {nameof(SpecialColumnPosition)} after the playfield is loaded requires re-creating the playfield."); + specialColumnPosition = value; + } + } - Add(new Box { RelativeSizeAxes = Axes.Both, Alpha = 0.5f }); + public readonly FlowContainer Columns; - for (int i = 0; i < columns; i++) - Add(new Box + private readonly ControlPointContainer barlineContainer; + + private List normalColumnColours = new List(); + private Color4 specialColumnColour; + + private readonly int columnCount; + + public ManiaPlayfield(int columnCount, IEnumerable timingChanges) + { + this.columnCount = columnCount; + + if (columnCount <= 0) + throw new ArgumentException("Can't have zero or fewer columns."); + + Children = new Drawable[] + { + new Container { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, - Size = new Vector2(2, 1), - RelativePositionAxes = Axes.Both, - Position = new Vector2((float)i / columns, 0), - Alpha = 0.5f, - Colour = Color4.Black - }); + AutoSizeAxes = Axes.X, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + Columns = new FillFlowContainer + { + Name = "Columns", + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Left = 1, Right = 1 }, + Spacing = new Vector2(1, 0) + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = HIT_TARGET_POSITION }, + Children = new[] + { + barlineContainer = new ControlPointContainer(timingChanges) + { + Name = "Bar lines", + RelativeSizeAxes = Axes.Both, + } + } + } + } + } + }; + + for (int i = 0; i < columnCount; i++) + Columns.Add(new Column(timingChanges)); + + TimeSpan = time_span_default; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + normalColumnColours = new List + { + colours.RedDark, + colours.GreenDark + }; + + specialColumnColour = colours.BlueDark; + + // Set the special column + colour + key + for (int i = 0; i < columnCount; i++) + { + Column column = Columns.Children.ElementAt(i); + column.IsSpecial = isSpecialColumn(i); + + if (!column.IsSpecial) + continue; + + column.Key = Key.Space; + column.AccentColour = specialColumnColour; + } + + var nonSpecialColumns = Columns.Children.Where(c => !c.IsSpecial).ToList(); + + // We'll set the colours of the non-special columns in a separate loop, because the non-special + // column colours are mirrored across their centre and special styles mess with this + for (int i = 0; i < Math.Ceiling(nonSpecialColumns.Count / 2f); i++) + { + Color4 colour = normalColumnColours[i % normalColumnColours.Count]; + nonSpecialColumns[i].AccentColour = colour; + nonSpecialColumns[nonSpecialColumns.Count - 1 - i].AccentColour = colour; + } + + // We'll set the keys for non-special columns in another separate loop because it's not mirrored like the above colours + // Todo: This needs to go when we get to bindings and use Button1, ..., ButtonN instead + for (int i = 0; i < nonSpecialColumns.Count; i++) + { + Column column = nonSpecialColumns[i]; + + int keyOffset = default_keys.Length / 2 - nonSpecialColumns.Count / 2 + i; + if (keyOffset >= 0 && keyOffset < default_keys.Length) + column.Key = default_keys[keyOffset]; + else + // There is no default key defined for this column. Let's set this to Unknown for now + // however note that this will be gone after bindings are in place + column.Key = Key.Unknown; + } + } + + /// + /// 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) + { + switch (SpecialColumnPosition) + { + default: + case SpecialColumnPosition.Normal: + return columnCount % 2 == 1 && column == columnCount / 2; + case SpecialColumnPosition.Left: + return column == 0; + case SpecialColumnPosition.Right: + return column == columnCount - 1; + } + } + + public override void Add(DrawableHitObject h) => Columns.Children.ElementAt(h.HitObject.Column).Add(h); + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (state.Keyboard.ControlPressed) + { + switch (args.Key) + { + case Key.Minus: + transformTimeSpanTo(TimeSpan + time_span_step, 200, EasingTypes.OutQuint); + break; + case Key.Plus: + transformTimeSpanTo(TimeSpan - time_span_step, 200, EasingTypes.OutQuint); + break; + } + } + + return false; + } + + private double timeSpan; + /// + /// The amount of time which the length of the playfield spans. + /// + public double TimeSpan + { + get { return timeSpan; } + set + { + if (timeSpan == value) + return; + timeSpan = value; + + timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max); + + barlineContainer.TimeSpan = value; + Columns.Children.ForEach(c => c.ControlPointContainer.TimeSpan = value); + } + } + + private void transformTimeSpanTo(double newTimeSpan, double duration = 0, EasingTypes easing = EasingTypes.None) + { + TransformTo(() => TimeSpan, newTimeSpan, duration, easing, new TransformTimeSpan()); + } + + private class TransformTimeSpan : Transform + { + public override double CurrentValue + { + get + { + double time = Time?.Current ?? 0; + if (time < StartTime) return StartValue; + if (time >= EndTime) return EndValue; + + return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); + } + } + + public override void Apply(Drawable d) + { + base.Apply(d); + + var p = (ManiaPlayfield)d; + p.TimeSpan = CurrentValue; + } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Mania/UI/SpecialColumnPosition.cs b/osu.Game.Rulesets.Mania/UI/SpecialColumnPosition.cs new file mode 100644 index 0000000000..7fd30e7d0d --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/SpecialColumnPosition.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Mania.UI +{ + public enum SpecialColumnPosition + { + /// + /// The special column will lie in the center of the columns. + /// + Normal, + /// + /// The special column will lie to the left of the columns. + /// + Left, + /// + /// The special column will lie to the right of the columns. + /// + Right + } +} diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index facffa757c..ec426c895f 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -47,19 +47,33 @@ + + + + + + + + + + + + - - + + + + diff --git a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs index ff277dea1f..cc06946d38 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModAutoplay : ModAutoplay { - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); protected override Score CreateReplayScore(Beatmap beatmap) => new Score { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 90a6d432c4..3722d13ffc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -1,15 +1,16 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using OpenTK; using OpenTK.Graphics; -using osu.Game.Rulesets.Osu.UI; +using osu.Game.Graphics; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Allocation; +using osu.Game.Screens.Ranking; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -18,9 +19,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Spinner spinner; private readonly SpinnerDisc disc; + private readonly SpinnerTicks ticks; + + private readonly Container mainContainer; + private readonly SpinnerBackground background; private readonly Container circleContainer; - private readonly DrawableHitCircle circle; + private readonly CirclePiece circle; + private readonly GlowPiece glow; + + private readonly TextAwesome symbol; + + private readonly Color4 baseColour = OsuColour.FromHex(@"002c3c"); + private readonly Color4 fillColour = OsuColour.FromHex(@"005b7c"); + + private Color4 normalColour; + private Color4 completeColour; public DrawableSpinner(Spinner s) : base(s) { @@ -29,57 +43,91 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; Position = s.Position; - //take up full playfield. - Size = new Vector2(OsuPlayfield.BASE_SIZE.X); + RelativeSizeAxes = Axes.Both; + + // we are slightly bigger than our parent, to clip the top and bottom of the circle + Height = 1.3f; spinner = s; Children = new Drawable[] { - background = new SpinnerBackground - { - Alpha = 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - DiscColour = Color4.Black - }, - disc = new SpinnerDisc - { - Alpha = 0, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - DiscColour = AccentColour - }, circleContainer = new Container { AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Children = new [] + Children = new Drawable[] { - circle = new DrawableHitCircle(s) + glow = new GlowPiece(), + circle = new CirclePiece { - Interactive = false, Position = Vector2.Zero, Anchor = Anchor.Centre, - } + }, + new RingPiece(), + symbol = new TextAwesome + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + UseFullGlyphHeight = true, + TextSize = 48, + Icon = FontAwesome.fa_asterisk, + Shadow = false, + }, } - } + }, + mainContainer = new AspectContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Children = new Drawable[] + { + background = new SpinnerBackground + { + Alpha = 0.6f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + disc = new SpinnerDisc(spinner) + { + Scale = Vector2.Zero, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + circleContainer.CreateProxy(), + ticks = new SpinnerTicks + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }, }; - - background.Scale = scaleToCircle; - disc.Scale = scaleToCircle; } + public float Progress => MathHelper.Clamp(disc.RotationAbsolute / 360 / spinner.SpinsRequired, 0, 1); + protected override void CheckJudgement(bool userTriggered) { if (Time.Current < HitObject.StartTime) return; - disc.ScaleTo(Interpolation.ValueAt(Math.Sqrt(Progress), scaleToCircle, Vector2.One, 0, 1), 100); - - if (Progress >= 1) + if (Progress >= 1 && !disc.Complete) + { disc.Complete = true; + const float duration = 200; + + disc.FadeAccent(completeColour, duration); + + background.FadeAccent(completeColour, duration); + background.FadeOut(duration); + + circle.FadeColour(completeColour, duration); + glow.FadeColour(completeColour, duration); + } + if (!userTriggered && Time.Current >= spinner.EndTime) { if (Progress >= 1) @@ -106,30 +154,48 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - private Vector2 scaleToCircle => circle.Scale * circle.DrawWidth / DrawWidth * 0.95f; + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + normalColour = baseColour; - private const float spins_per_minute_needed = 100 + 5 * 15; //TODO: read per-map OD and place it on the 5 + background.AccentColour = normalColour; - private float rotationsNeeded => (float)(spins_per_minute_needed * (spinner.EndTime - spinner.StartTime) / 60000f); + completeColour = colours.YellowLight.Opacity(0.75f); - public float Progress => MathHelper.Clamp(disc.RotationAbsolute / 360 / rotationsNeeded, 0, 1); + disc.AccentColour = fillColour; + circle.Colour = colours.BlueDark; + glow.Colour = colours.BlueDark; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + circle.Rotation = disc.Rotation; + ticks.Rotation = disc.Rotation; + + float relativeCircleScale = spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; + disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, EasingTypes.OutQuint); + + symbol.RotateTo(disc.Rotation / 2, 500, EasingTypes.OutQuint); + } protected override void UpdatePreemptState() { base.UpdatePreemptState(); - circleContainer.ScaleTo(1, 400, EasingTypes.OutElastic); + circleContainer.ScaleTo(spinner.Scale * 0.3f); + circleContainer.ScaleTo(spinner.Scale, TIME_PREEMPT / 1.4f, EasingTypes.OutQuint); - background.Delay(TIME_PREEMPT - 500); + disc.RotateTo(-720); + symbol.RotateTo(-720); - background.ScaleTo(scaleToCircle * 1.2f, 400, EasingTypes.OutQuint); - background.FadeIn(200); + mainContainer.ScaleTo(0); + mainContainer.ScaleTo(spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, TIME_PREEMPT - 150, EasingTypes.OutQuint); - background.Delay(400); - background.ScaleTo(1, 250, EasingTypes.OutQuint); - - disc.Delay(TIME_PREEMPT - 50); - disc.FadeIn(200); + mainContainer.Delay(TIME_PREEMPT - 150); + mainContainer.ScaleTo(1, 500, EasingTypes.OutQuint); } protected override void UpdateCurrentState(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index 9a90c07517..3004dafda7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { private readonly Sprite disc; - public Func Hit; public CirclePiece() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index b23fdde4e8..8b9441ea65 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -97,8 +97,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - snakingIn = config.GetBindable(OsuConfig.SnakingInSliders); - snakingOut = config.GetBindable(OsuConfig.SnakingOutSliders); + snakingIn = config.GetBindable(OsuSetting.SnakingInSliders); + snakingOut = config.GetBindable(OsuSetting.SnakingOutSliders); reloadTexture(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs index 72024bbe99..66cf7758b9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs @@ -1,10 +1,55 @@ // Copyright (c) 2007-2017 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.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; + namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class SpinnerBackground : SpinnerDisc + public class SpinnerBackground : CircularContainer, IHasAccentColour { public override bool HandleInput => false; + + protected Box Disc; + + public Color4 AccentColour + { + get + { + return Disc.Colour; + } + set + { + Disc.Colour = value; + + EdgeEffect = new EdgeEffect + { + Hollow = true, + Type = EdgeEffectType.Glow, + Radius = 40, + Colour = value, + }; + } + } + + public SpinnerBackground() + { + RelativeSizeAxes = Axes.Both; + Masking = true; + + Children = new Drawable[] + { + Disc = new Box + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Alpha = 1, + }, + }; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 71adba74c7..29d6d1f147 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -2,13 +2,8 @@ // 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.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Game.Graphics; @@ -17,104 +12,31 @@ using OpenTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class SpinnerDisc : CircularContainer + public class SpinnerDisc : CircularContainer, IHasAccentColour { - protected Sprite Disc; + private readonly Spinner spinner; - public SRGBColour DiscColour + public Color4 AccentColour { - get { return Disc.Colour; } - set { Disc.Colour = value; } + get { return background.AccentColour; } + set { background.AccentColour = value; } } - private Color4 completeColour; + private readonly SpinnerBackground background; - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private const float idle_alpha = 0.2f; + private const float tracking_alpha = 0.4f; + + public SpinnerDisc(Spinner s) { - completeColour = colours.YellowLight.Opacity(0.8f); - Masking = true; - } + spinner = s; - private class SpinnerBorder : Container - { - public SpinnerBorder() - { - Origin = Anchor.Centre; - Anchor = Anchor.Centre; - RelativeSizeAxes = Axes.Both; - - layout(); - } - - private int lastLayoutDotCount; - private void layout() - { - int count = (int)(MathHelper.Pi * ScreenSpaceDrawQuad.Width / 9); - - if (count == lastLayoutDotCount) return; - - lastLayoutDotCount = count; - - while (Children.Count() < count) - { - Add(new CircularContainer - { - Colour = Color4.White, - RelativePositionAxes = Axes.Both, - Masking = true, - Origin = Anchor.Centre, - Size = new Vector2(1 / ScreenSpaceDrawQuad.Width * 2000), - Children = new[] - { - new Box - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - } - } - }); - } - - var size = new Vector2(1 / ScreenSpaceDrawQuad.Width * 2000); - - int i = 0; - foreach (var d in Children) - { - d.Size = size; - d.Position = new Vector2( - 0.5f + (float)Math.Sin((float)i / count * 2 * MathHelper.Pi) / 2, - 0.5f + (float)Math.Cos((float)i / count * 2 * MathHelper.Pi) / 2 - ); - - i++; - } - } - - protected override void Update() - { - base.Update(); - layout(); - } - } - - public SpinnerDisc() - { AlwaysReceiveInput = true; - RelativeSizeAxes = Axes.Both; Children = new Drawable[] { - Disc = new Box - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Alpha = 0.2f, - }, - new SpinnerBorder() + background = new SpinnerBackground { Alpha = idle_alpha }, }; } @@ -125,10 +47,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces set { if (value == tracking) return; - tracking = value; - Disc.FadeTo(tracking ? 0.5f : 0.2f, 100); + background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100); } } @@ -139,31 +60,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces set { if (value == complete) return; - complete = value; - Disc.FadeColour(completeColour, 200); - updateCompleteTick(); } } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - Tracking = true; + Tracking |= state.Mouse.HasMainButtonPressed; return base.OnMouseDown(state, args); } protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) { - Tracking = false; + Tracking &= state.Mouse.HasMainButtonPressed; return base.OnMouseUp(state, args); } protected override bool OnMouseMove(InputState state) { Tracking |= state.Mouse.HasMainButtonPressed; - mousePosition = state.Mouse.Position; + mousePosition = Parent.ToLocalSpace(state.Mouse.NativeState.Position); return base.OnMouseMove(state); } @@ -177,13 +95,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private bool updateCompleteTick() => completeTick != (completeTick = (int)(RotationAbsolute / 360)); + private bool rotationTransferred; + protected override void Update() { base.Update(); var thisAngle = -(float)MathHelper.RadiansToDegrees(Math.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); - if (tracking) + + bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + + if (validAndTracking) { + if (!rotationTransferred) + { + currentRotation = Rotation * 2; + rotationTransferred = true; + } + if (thisAngle - lastAngle > 180) lastAngle += 360; else if (lastAngle - thisAngle > 180) @@ -192,17 +121,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces currentRotation += thisAngle - lastAngle; RotationAbsolute += Math.Abs(thisAngle - lastAngle); } + lastAngle = thisAngle; if (Complete && updateCompleteTick()) { - Disc.Flush(flushType: typeof(TransformAlpha)); - Disc.FadeTo(0.75f, 30, EasingTypes.OutExpo); - Disc.Delay(30); - Disc.FadeTo(0.5f, 250, EasingTypes.OutQuint); + background.Flush(flushType: typeof(TransformAlpha)); + background.FadeTo(tracking_alpha + 0.2f, 60, EasingTypes.OutExpo); + background.Delay(60); + background.FadeTo(tracking_alpha, 250, EasingTypes.OutQuint); } - RotateTo(currentRotation, 100, EasingTypes.OutExpo); + RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, EasingTypes.OutExpo); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs new file mode 100644 index 0000000000..4dbb6bd4d6 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + public class SpinnerTicks : Container + { + public SpinnerTicks() + { + Origin = Anchor.Centre; + Anchor = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + + const int count = 18; + + for (int i = 0; i < count; i++) + { + Add(new Container + { + Colour = Color4.Black, + Alpha = 0.4f, + EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Glow, + Radius = 10, + Colour = Color4.Gray.Opacity(0.2f), + }, + RelativePositionAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Size = new Vector2(60, 10), + Origin = Anchor.Centre, + Position = new Vector2( + 0.5f + (float)Math.Sin((float)i / count * 2 * MathHelper.Pi) / 2 * 0.86f, + 0.5f + (float)Math.Cos((float)i / count * 2 * MathHelper.Pi) / 2 * 0.86f + ), + Rotation = -(float)i / count * 360 + 90, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 0a2c05833a..3761b62b65 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Rulesets.Objects.Types; +using osu.Game.Beatmaps.Timing; +using osu.Game.Database; namespace osu.Game.Rulesets.Osu.Objects { @@ -10,6 +12,18 @@ namespace osu.Game.Rulesets.Osu.Objects public double EndTime { get; set; } public double Duration => EndTime - StartTime; + /// + /// Number of spins required to finish the spinner without miss. + /// + public int SpinsRequired { get; protected set; } = 1; + public override bool NewCombo => true; + + public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty) + { + base.ApplyDefaults(timing, difficulty); + + SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5)); + } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 39e911651a..af4a099e0d 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu { public class OsuRuleset : Ruleset { - public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap) => new OsuHitRenderer(beatmap); + public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new OsuHitRenderer(beatmap, isForCurrentRuleset); public override IEnumerable GetBeatmapStatistics(WorkingBeatmap beatmap) => new[] { diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index f8365bf9ab..5ede3f56f5 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -91,25 +91,25 @@ namespace osu.Game.Rulesets.Osu.Replays // Make the cursor stay at a hitObject as long as possible (mainly for autopilot). if (h.StartTime - h.HitWindowFor(OsuScoreResult.Miss) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.EndPosition.X, prev.EndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Miss), h.Position.X, h.Position.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); + if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Miss), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); } else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.EndPosition.X, prev.EndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50), h.Position.X, h.Position.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); + if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); } else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100) > endTime + h.HitWindowFor(OsuScoreResult.Hit100) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit100), prev.EndPosition.X, prev.EndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100), h.Position.X, h.Position.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit100), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); + if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); } } private void addHitObjectReplay(OsuHitObject h) { // Default values for circles/sliders - Vector2 startPosition = h.Position; + Vector2 startPosition = h.StackedPosition; EasingTypes easing = preferredEasing; float spinnerDirection = -1; @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Osu.Replays // 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.EndPosition.X, h.EndPosition.Y, ReplayButtonState.None); + ReplayFrame endFrame = new ReplayFrame(hEndTime + endDelay, h.StackedEndPosition.X, h.StackedEndPosition.Y, ReplayButtonState.None); // Decrement because we want the previous frame, not the next one int index = FindInsertionIndex(startFrame) - 1; diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index d2cd7a1156..65ed9530f2 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Osu.Replays { public int Compare(ReplayFrame f1, ReplayFrame f2) { - if (f1 == null) throw new NullReferenceException($@"{nameof(f1)} cannot be null"); - if (f2 == null) throw new NullReferenceException($@"{nameof(f2)} cannot be null"); + if (f1 == null) throw new ArgumentNullException(nameof(f1)); + if (f2 == null) throw new ArgumentNullException(nameof(f2)); return f1.Time.CompareTo(f2.Time); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuHitRenderer.cs b/osu.Game.Rulesets.Osu/UI/OsuHitRenderer.cs index 687518e6d5..e582d2fcd3 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuHitRenderer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuHitRenderer.cs @@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Osu.UI { public class OsuHitRenderer : HitRenderer { - public OsuHitRenderer(WorkingBeatmap beatmap) - : base(beatmap) + public OsuHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(beatmap, isForCurrentRuleset) { } diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 8974b1bcbd..b91bdc6a78 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -64,6 +64,7 @@ + @@ -103,9 +104,7 @@ - - - +