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 6918afa620..f285ebde67 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,37 +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": [ - // Ask msbuild to generate full paths for file names. - "/property:GenerateFullPaths=true", - "/property:DebugType=portable" - ], - // 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" - ], - // 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/README.md b/README.md index 885c7c7722..ad56178132 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ -# osu! [![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) +# osu! [![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) - - -[osu! on the web](https://osu.ppy.sh) | [dev chat](https://discord.gg/ppy) - -Rhythm is just a *click* away. The future of osu! and the beginning of an open era! +Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! # Status @@ -12,14 +8,14 @@ This is still heavily under development and is not intended for end-user use. Th # Requirements -- A desktop platform which can compile .NET 4.5. -- Visual Studio or MonoDevelop is recommended. +- A desktop platform which can compile .NET 4.5 (tested on macOS, linux and windows). We recommend using [Visual Studio Code](https://code.visualstudio.com/) (all platforms) or [Visual Studio Community Edition](https://www.visualstudio.com/) (windows only), both of which are free. +- Make sure you initialise and keep submodules up-to-date. # Contributing We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention on having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time, to ensure no effort is wasted. -Contributions can be made via pull requests to this repository. We hope to credit and reward larger contributions via a [bounty system](https://goo.gl/nFdoyI). If you're unsure of what you can help with, check out the [list](https://github.com/ppy/osu/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Abounty) of available issues with bounty. +Contributions can be made via pull requests to this repository. We hope to credit and reward larger contributions via a [bounty system](https://www.bountysource.com/teams/ppy). If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu-framework/issues). Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. I welcome all feedback so we can make contributing to this project as pain-free as possible. diff --git a/osu-framework b/osu-framework index 9204b83850..67f3958036 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 9204b838504a51ffe7577c103b91270a2687bfb8 +Subproject commit 67f39580365f7d0a42f8788eae2b60881dde1c67 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..1049a8818a 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[] diff --git a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs index 15b38b3e83..dceb7a9cff 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs +++ b/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Framework.Timing; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; @@ -62,15 +61,12 @@ namespace osu.Desktop.VisualTests.Tests add(new DrawableSlider(new Slider { StartTime = framedClock.CurrentTime + 600, - CurveObject = new CurvedHitObject + ControlPoints = new List { - ControlPoints = new List - { - new Vector2(-200, 0), - new Vector2(400, 0), - }, - Distance = 400 + new Vector2(-200, 0), + new Vector2(400, 0), }, + Distance = 400, Position = new Vector2(-200, 0), Velocity = 1, TickDistance = 100, diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseManiaHitObjects.cs new file mode 100644 index 0000000000..3ad83beb73 --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseManiaHitObjects.cs @@ -0,0 +1,89 @@ +// 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 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/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/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 7c40d21512..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, @@ -38,9 +42,9 @@ namespace osu.Desktop.VisualTests.Tests Origin = Anchor.TopLeft, }); - AddStep("Toggle Bar", progress.ToggleBar); + AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking); AddWaitStep(5); - AddStep("Toggle Bar", progress.ToggleBar); + AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking); AddWaitStep(2); AddRepeatStep("New Values", displayNewValues, 5); @@ -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/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 135e4596c7..dbb1750b72 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.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 847af965cc..e51bbcdc13 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -8,16 +8,28 @@ using System; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Beatmaps; using osu.Game.Rulesets.Objects; +using OpenTK; namespace osu.Game.Rulesets.Mania.Beatmaps { - internal class ManiaBeatmapConverter : BeatmapConverter + internal class ManiaBeatmapConverter : BeatmapConverter { protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; - protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) { - yield return null; + int availableColumns = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.CircleSize); + + var positionData = original as IHasXPosition; + + float localWDivisor = 512.0f / availableColumns; + int column = MathHelper.Clamp((int)Math.Floor((positionData?.X ?? 1) / localWDivisor), 0, availableColumns - 1); + + yield return new Note + { + StartTime = original.StartTime, + Column = column, + }; } } } 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/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..767a2b3458 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.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 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.BottomCentre + } + }); + } + + 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) + { + } + } +} 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..0307e9162a --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -0,0 +1,64 @@ +// 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.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; + + private readonly Container glowContainer; + + protected DrawableManiaHitObject(TObject hitObject) + : base(hitObject) + { + HitObject = hitObject; + + RelativePositionAxes = Axes.Y; + Y = (float)HitObject.StartTime; + + Add(glowContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + } + }); + } + + public override Color4 AccentColour + { + get { return base.AccentColour; } + set + { + if (base.AccentColour == value) + return; + base.AccentColour = value; + + glowContainer.EdgeEffect = new EdgeEffect + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = 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..ce61a7a86f --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs @@ -0,0 +1,48 @@ +// 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; + Masking = true; + + 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/ManiaHitObject.cs similarity index 60% rename from osu.Game.Rulesets.Mania/Objects/ManiaBaseHit.cs rename to osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs index 4c15b69eb7..93aaa94f45 100644 --- a/osu.Game.Rulesets.Mania/Objects/ManiaBaseHit.cs +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObject.cs @@ -1,12 +1,13 @@ // 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 ManiaBaseHit : HitObject + public abstract class ManiaHitObject : HitObject, IHasColumn { - public int Column; + public int Column { 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..986aefb2bd 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs @@ -1,34 +1,88 @@ // 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) + public ManiaHitRenderer(WorkingBeatmap beatmap) : base(beatmap) { - 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.Last() as IHasEndTime)?.EndTime ?? Objects.Last().StartTime; + + // 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 note = h as Note; + if (note != null) + return new DrawableNote(note); - protected override DrawableHitObject GetVisualRepresentation(ManiaBaseHit h) => null; + 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..00deaba85d 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -48,18 +48,27 @@ + + + + + + + - - + + + + diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 0eece7fc4c..7f6f524a7a 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -19,10 +19,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) { - IHasCurve curveData = original as IHasCurve; - IHasEndTime endTimeData = original as IHasEndTime; - IHasPosition positionData = original as IHasPosition; - IHasCombo comboData = original as IHasCombo; + var curveData = original as IHasCurve; + var endTimeData = original as IHasEndTime; + var positionData = original as IHasPosition; + var comboData = original as IHasCombo; if (curveData != null) { @@ -30,7 +30,11 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { StartTime = original.StartTime, Samples = original.Samples, - CurveObject = curveData, + ControlPoints = curveData.ControlPoints, + CurveType = curveData.CurveType, + Distance = curveData.Distance, + RepeatSamples = curveData.RepeatSamples, + RepeatCount = curveData.RepeatCount, Position = positionData?.Position ?? Vector2.Zero, NewCombo = comboData?.NewCombo ?? false }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs index bdb5f386d0..cc06946d38 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs @@ -3,6 +3,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using System; @@ -91,11 +92,11 @@ 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 { - Replay = new OsuAutoReplay(beatmap) + Replay = new OsuAutoGenerator(beatmap).Generate() }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index e1276f30c4..9f8ff17853 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -12,16 +12,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { public class FollowPoint : Container { - public double StartTime; - public double EndTime; - public Vector2 EndPosition; - private const float width = 8; public FollowPoint() { Origin = Anchor.Centre; - Alpha = 0; Masking = true; AutoSizeAxes = Axes.Both; @@ -45,22 +40,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections }, }; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Delay(StartTime); - FadeIn(DrawableOsuHitObject.TIME_FADEIN); - ScaleTo(1.5f); - ScaleTo(1, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out); - MoveTo(EndPosition, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out); - - Delay(EndTime - StartTime); - FadeOut(DrawableOsuHitObject.TIME_FADEIN); - - Delay(DrawableOsuHitObject.TIME_FADEIN); - Expire(true); - } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index a4e032050e..925767b851 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using OpenTK; +using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections @@ -80,14 +81,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections double fadeOutTime = startTime + fraction * duration; double fadeInTime = fadeOutTime - PreEmpt; - Add(new FollowPoint + FollowPoint fp; + + Add(fp = new FollowPoint { - StartTime = fadeInTime, - EndTime = fadeOutTime, Position = pointStartPosition, - EndPosition = pointEndPosition, Rotation = rotation, + Alpha = 0, + Scale = new Vector2(1.5f), }); + + using (fp.BeginAbsoluteSequence(fadeInTime)) + { + fp.FadeIn(DrawableOsuHitObject.TIME_FADEIN); + fp.ScaleTo(1, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out); + + fp.MoveTo(pointEndPosition, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out); + + fp.Delay(fadeOutTime - fadeInTime); + fp.FadeOut(DrawableOsuHitObject.TIME_FADEIN); + } + + fp.Expire(true); } } prevHitObject = currHitObject; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 4c1a74c675..09bfffeefe 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -104,10 +104,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ApproachCircle.ScaleTo(1.1f, TIME_PREEMPT); } - protected override void UpdateState(ArmedState state) + protected override void UpdateCurrentState(ArmedState state) { - base.UpdateState(state); - ApproachCircle.FadeOut(); double endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 2baf651cc0..57a9804330 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -21,17 +21,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override OsuJudgement CreateJudgement() => new OsuJudgement { MaxScore = OsuScoreResult.Hit300 }; - protected override void UpdateState(ArmedState state) + protected sealed override void UpdateState(ArmedState state) { Flush(); UpdateInitialState(); - Delay(HitObject.StartTime - Time.Current - TIME_PREEMPT + Judgement.TimeOffset, true); + using (BeginAbsoluteSequence(HitObject.StartTime - TIME_PREEMPT, true)) + { + UpdatePreemptState(); - UpdatePreemptState(); + using (BeginDelayedSequence(TIME_PREEMPT + Judgement.TimeOffset, true)) + UpdateCurrentState(state); + } + } - Delay(TIME_PREEMPT, true); + protected virtual void UpdateCurrentState(ArmedState state) + { } protected virtual void UpdatePreemptState() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index ed698f5ad3..b80f1d7178 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -158,10 +158,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ball.Alpha = 0; } - protected override void UpdateState(ArmedState state) + protected override void UpdateCurrentState(ArmedState state) { - base.UpdateState(state); - ball.FadeIn(); Delay(slider.Duration, true); @@ -181,4 +179,4 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { void UpdateProgress(double progress, int repeat); } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 86baf9f235..6b4d40e080 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -72,10 +72,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Delay(-animIn); } - protected override void UpdateState(ArmedState state) + protected override void UpdateCurrentState(ArmedState state) { - base.UpdateState(state); - switch (state) { case ArmedState.Idle: @@ -93,4 +91,4 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 4623fe7f22..a8ff231cc7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -108,11 +108,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Vector2 scaleToCircle => circle.Scale * circle.DrawWidth / DrawWidth * 0.95f; - private const float spins_per_minute_needed = 100 + 5 * 15; //TODO: read per-map OD and place it on the 5 - - private float rotationsNeeded => (float)(spins_per_minute_needed * (spinner.EndTime - spinner.StartTime) / 60000f); - - public float Progress => MathHelper.Clamp(disc.RotationAbsolute / 360 / rotationsNeeded, 0, 1); + public float Progress => MathHelper.Clamp(disc.RotationAbsolute / 360 / spinner.SpinsRequired, 0, 1); protected override void UpdatePreemptState() { @@ -132,10 +128,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables disc.FadeIn(200); } - protected override void UpdateState(ArmedState state) + protected override void UpdateCurrentState(ArmedState state) { - base.UpdateState(state); - Delay(spinner.Duration, true); FadeOut(160); 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/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 167bf21670..6c0147a3de 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -20,24 +20,33 @@ namespace osu.Game.Rulesets.Osu.Objects /// private const float base_scoring_distance = 100; - public IHasCurve CurveObject { get; set; } - - public SliderCurve Curve => CurveObject.Curve; + public readonly SliderCurve Curve = new SliderCurve(); public double EndTime => StartTime + RepeatCount * Curve.Distance / Velocity; public double Duration => EndTime - StartTime; public override Vector2 EndPosition => PositionAt(1); - public Vector2 PositionAt(double progress) => CurveObject.PositionAt(progress); - public double ProgressAt(double progress) => CurveObject.ProgressAt(progress); - public int RepeatAt(double progress) => CurveObject.RepeatAt(progress); + public List ControlPoints + { + get { return Curve.ControlPoints; } + set { Curve.ControlPoints = value; } + } - public List ControlPoints => CurveObject.ControlPoints; - public CurveType CurveType => CurveObject.CurveType; - public double Distance => CurveObject.Distance; + public CurveType CurveType + { + get { return Curve.CurveType; } + set { Curve.CurveType = value; } + } - public int RepeatCount => CurveObject.RepeatCount; + public double Distance + { + get { return Curve.Distance; } + set { Curve.Distance = value; } + } + + public List RepeatSamples { get; set; } = new List(); + public int RepeatCount { get; set; } = 1; private int stackHeight; public override int StackHeight @@ -63,6 +72,18 @@ namespace osu.Game.Rulesets.Osu.Objects TickDistance = scoringDistance / difficulty.SliderTickRate; } + public Vector2 PositionAt(double progress) => Curve.PositionAt(ProgressAt(progress)); + + public double ProgressAt(double progress) + { + double p = progress * RepeatCount % 1; + if (RepeatAt(progress) % 2 == 1) + p = 1 - p; + return p; + } + + public int RepeatAt(double progress) => (int)(progress * RepeatCount); + public IEnumerable Ticks { get @@ -96,12 +117,12 @@ namespace osu.Game.Rulesets.Osu.Objects StackHeight = StackHeight, Scale = Scale, ComboColour = ComboColour, - Samples = Samples.Select(s => new SampleInfo + Samples = new SampleInfoList(Samples.Select(s => new SampleInfo { Bank = s.Bank, Name = @"slidertick", Volume = s.Volume - }).ToList() + })) }; } } 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/OsuAutoReplay.cs b/osu.Game.Rulesets.Osu/OsuAutoReplay.cs deleted file mode 100644 index da30cf4efb..0000000000 --- a/osu.Game.Rulesets.Osu/OsuAutoReplay.cs +++ /dev/null @@ -1,315 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using OpenTK; -using osu.Framework.MathUtils; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Replays; -using osu.Game.Users; - -namespace osu.Game.Rulesets.Osu -{ - public class OsuAutoReplay : Replay - { - private static readonly Vector2 spinner_centre = new Vector2(256, 192); - - private const float spin_radius = 50; - - private readonly Beatmap beatmap; - - public OsuAutoReplay(Beatmap beatmap) - { - this.beatmap = beatmap; - - User = new User - { - Username = @"Autoplay", - }; - - createAutoReplay(); - } - - private class ReplayFrameComparer : IComparer - { - 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"); - - return f1.Time.CompareTo(f2.Time); - } - } - - private static readonly IComparer replay_frame_comparer = new ReplayFrameComparer(); - - private int findInsertionIndex(ReplayFrame frame) - { - int index = Frames.BinarySearch(frame, replay_frame_comparer); - - if (index < 0) - { - index = ~index; - } - else - { - // Go to the first index which is actually bigger - while (index < Frames.Count && frame.Time == Frames[index].Time) - { - ++index; - } - } - - return index; - } - - private void addFrameToReplay(ReplayFrame frame) => Frames.Insert(findInsertionIndex(frame), frame); - - private static Vector2 circlePosition(double t, double radius) => new Vector2((float)(Math.Cos(t) * radius), (float)(Math.Sin(t) * radius)); - - private double applyModsToTime(double v) => v; - private double applyModsToRate(double v) => v; - - public bool DelayedMovements; // ModManager.CheckActive(Mods.Relax2); - - private void createAutoReplay() - { - int buttonIndex = 0; - - EasingTypes preferredEasing = DelayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out; - - addFrameToReplay(new ReplayFrame(-100000, 256, 500, ReplayButtonState.None)); - addFrameToReplay(new ReplayFrame(beatmap.HitObjects[0].StartTime - 1500, 256, 500, ReplayButtonState.None)); - addFrameToReplay(new ReplayFrame(beatmap.HitObjects[0].StartTime - 1000, 256, 192, ReplayButtonState.None)); - - // We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps. - float frameDelay = (float)applyModsToRate(1000.0 / 60.0); - - // Already superhuman, but still somewhat realistic - int reactionTime = (int)applyModsToRate(100); - - - for (int i = 0; i < beatmap.HitObjects.Count; i++) - { - OsuHitObject h = beatmap.HitObjects[i]; - - //if (h.EndTime < InputManager.ReplayStartTime) - //{ - // h.IsHit = true; - // continue; - //} - - int endDelay = h is Spinner ? 1 : 0; - - if (DelayedMovements && i > 0) - { - OsuHitObject last = beatmap.HitObjects[i - 1]; - - double endTime = (last as IHasEndTime)?.EndTime ?? last.StartTime; - - //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 (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.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)); - } - else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50) - { - if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.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)); - } - else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100) > endTime + h.HitWindowFor(OsuScoreResult.Hit100) + 50) - { - if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit100), last.EndPosition.X, last.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)); - } - } - - - Vector2 targetPosition = h.Position; - EasingTypes easing = preferredEasing; - float spinnerDirection = -1; - - if (h is Spinner) - { - targetPosition = Frames[Frames.Count - 1].Position; - - Vector2 difference = spinner_centre - targetPosition; - - float differenceLength = difference.Length; - float newLength = (float)Math.Sqrt(differenceLength * differenceLength - spin_radius * spin_radius); - - if (differenceLength > spin_radius) - { - float angle = (float)Math.Asin(spin_radius / differenceLength); - - if (angle > 0) - { - spinnerDirection = -1; - } - else - { - spinnerDirection = 1; - } - - difference.X = difference.X * (float)Math.Cos(angle) - difference.Y * (float)Math.Sin(angle); - difference.Y = difference.X * (float)Math.Sin(angle) + difference.Y * (float)Math.Cos(angle); - - difference.Normalize(); - difference *= newLength; - - targetPosition += difference; - - easing = EasingTypes.In; - } - else if (difference.Length > 0) - { - targetPosition = spinner_centre - difference * (spin_radius / difference.Length); - } - else - { - targetPosition = spinner_centre + new Vector2(0, -spin_radius); - } - } - - - // Do some nice easing for cursor movements - if (Frames.Count > 0) - { - ReplayFrame lastFrame = Frames[Frames.Count - 1]; - - // Wait until Auto could "see and react" to the next note. - double waitTime = h.StartTime - Math.Max(0.0, DrawableOsuHitObject.TIME_PREEMPT - reactionTime); - if (waitTime > lastFrame.Time) - { - lastFrame = new ReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState); - addFrameToReplay(lastFrame); - } - - Vector2 lastPosition = lastFrame.Position; - - double timeDifference = applyModsToTime(h.StartTime - lastFrame.Time); - - // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. - if (timeDifference > 0 && // Sanity checks - ((lastPosition - targetPosition).Length > h.Radius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough - timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. - { - // Perform eased movement - for (double time = lastFrame.Time + frameDelay; time < h.StartTime; time += frameDelay) - { - Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPosition, lastFrame.Time, h.StartTime, easing); - addFrameToReplay(new ReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState)); - } - - buttonIndex = 0; - } - else - { - buttonIndex++; - } - } - - ReplayButtonState button = buttonIndex % 2 == 0 ? ReplayButtonState.Left1 : ReplayButtonState.Right1; - - double hEndTime = ((h as IHasEndTime)?.EndTime ?? h.StartTime) + KEY_UP_DELAY; - - ReplayFrame newFrame = new ReplayFrame(h.StartTime, targetPosition.X, targetPosition.Y, button); - ReplayFrame endFrame = new ReplayFrame(hEndTime + endDelay, h.EndPosition.X, h.EndPosition.Y, ReplayButtonState.None); - - // Decrement because we want the previous frame, not the next one - int index = findInsertionIndex(newFrame) - 1; - - // Do we have a previous frame? No need to check for < replay.Count since we decremented! - if (index >= 0) - { - ReplayFrame previousFrame = Frames[index]; - var previousButton = previousFrame.ButtonState; - - // If a button is already held, then we simply alternate - if (previousButton != ReplayButtonState.None) - { - Debug.Assert(previousButton != (ReplayButtonState.Left1 | ReplayButtonState.Right1)); - - // Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button. - if (previousButton == button) - { - button = (ReplayButtonState.Left1 | ReplayButtonState.Right1) & ~button; - newFrame.ButtonState = button; - } - - // We always follow the most recent slider / spinner, so remove any other frames that occur while it exists. - int endIndex = findInsertionIndex(endFrame); - - if (index < Frames.Count - 1) - Frames.RemoveRange(index + 1, Math.Max(0, endIndex - (index + 1))); - - // After alternating we need to keep holding the other button in the future rather than the previous one. - for (int j = index + 1; j < Frames.Count; ++j) - { - // Don't affect frames which stop pressing a button! - if (j < Frames.Count - 1 || Frames[j].ButtonState == previousButton) - Frames[j].ButtonState = button; - } - } - } - - addFrameToReplay(newFrame); - - // We add intermediate frames for spinning / following a slider here. - if (h is Spinner) - { - Spinner s = h as Spinner; - - Vector2 difference = targetPosition - spinner_centre; - - float radius = difference.Length; - float angle = radius == 0 ? 0 : (float)Math.Atan2(difference.Y, difference.X); - - double t; - - for (double j = h.StartTime + frameDelay; j < s.EndTime; j += frameDelay) - { - t = applyModsToTime(j - h.StartTime) * spinnerDirection; - - Vector2 pos = spinner_centre + circlePosition(t / 20 + angle, spin_radius); - addFrameToReplay(new ReplayFrame((int)j, pos.X, pos.Y, button)); - } - - t = applyModsToTime(s.EndTime - h.StartTime) * spinnerDirection; - Vector2 endPosition = spinner_centre + circlePosition(t / 20 + angle, spin_radius); - - addFrameToReplay(new ReplayFrame(s.EndTime, endPosition.X, endPosition.Y, button)); - - endFrame.MouseX = endPosition.X; - endFrame.MouseY = endPosition.Y; - } - else if (h is Slider) - { - Slider s = h as Slider; - - for (double j = frameDelay; j < s.Duration; j += frameDelay) - { - Vector2 pos = s.PositionAt(j / s.Duration); - addFrameToReplay(new ReplayFrame(h.StartTime + j, pos.X, pos.Y, button)); - } - - addFrameToReplay(new ReplayFrame(s.EndTime, s.EndPosition.X, s.EndPosition.Y, button)); - } - - // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! - if (Frames[Frames.Count - 1].Time <= endFrame.Time) - addFrameToReplay(endFrame); - } - - //Player.currentScore.Replay = InputManager.ReplayScore.Replay; - //Player.currentScore.PlayerName = "osu!"; - } - } -} diff --git a/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs index 14b890a055..5669993e67 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs @@ -3,7 +3,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Beatmaps; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using System; @@ -28,7 +27,7 @@ namespace osu.Game.Rulesets.Osu protected override void PreprocessHitObjects() { foreach (var h in Objects) - (h as IHasCurve)?.Curve?.Calculate(); + (h as Slider)?.Curve?.Calculate(); } protected override double CalculateInternal(Dictionary categoryDifficulty) @@ -190,4 +189,4 @@ namespace osu.Game.Rulesets.Osu Aim, }; } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Osu/OsuKeyConversionInputManager.cs b/osu.Game.Rulesets.Osu/OsuKeyConversionInputManager.cs index e71f15cd65..d60aab90fb 100644 --- a/osu.Game.Rulesets.Osu/OsuKeyConversionInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuKeyConversionInputManager.cs @@ -2,10 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Input; -using osu.Game.Configuration; using osu.Game.Screens.Play; using OpenTK.Input; using KeyboardState = osu.Framework.Input.KeyboardState; @@ -17,13 +14,6 @@ namespace osu.Game.Rulesets.Osu { private bool leftViaKeyboard; private bool rightViaKeyboard; - private Bindable mouseDisabled; - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - mouseDisabled = config.GetBindable(OsuConfig.MouseDisableButtons); - } protected override void TransformState(InputState state) { @@ -40,12 +30,6 @@ namespace osu.Game.Rulesets.Osu if (mouse != null) { - if (mouseDisabled.Value) - { - mouse.SetPressed(MouseButton.Left, false); - mouse.SetPressed(MouseButton.Right, false); - } - if (leftViaKeyboard) mouse.SetPressed(MouseButton.Left, true); if (rightViaKeyboard) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs new file mode 100644 index 0000000000..f8365bf9ab --- /dev/null +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -0,0 +1,332 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using System; +using System.Diagnostics; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.Osu.Replays +{ + public class OsuAutoGenerator : OsuAutoGeneratorBase + { + #region Parameters + + /// + /// If delayed movements should be used, causing the cursor to stay on each hitobject for as long as possible. + /// Mainly for Autopilot. + /// + public bool DelayedMovements; // ModManager.CheckActive(Mods.Relax2); + + #endregion + + #region Constants + + /// + /// The "reaction time" in ms between "seeing" a new hit object and moving to "react" to it. + /// + private readonly double reactionTime; + + /// + /// What easing to use when moving between hitobjects + /// + private EasingTypes preferredEasing => DelayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out; + + #endregion + + #region Construction / Initialisation + + public OsuAutoGenerator(Beatmap beatmap) + : base(beatmap) + { + // Already superhuman, but still somewhat realistic + reactionTime = ApplyModsToRate(100); + } + + #endregion + + #region Generator + + /// + /// Which button (left or right) to use for the current hitobject. + /// Even means LMB will be used to click, odd means RMB will be used. + /// This keeps track of the button previously used for alt/singletap logic. + /// + private int buttonIndex; + + public override Replay Generate() + { + buttonIndex = 0; + + AddFrameToReplay(new ReplayFrame(-100000, 256, 500, ReplayButtonState.None)); + AddFrameToReplay(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, 256, 500, ReplayButtonState.None)); + AddFrameToReplay(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, 256, 192, ReplayButtonState.None)); + + for (int i = 0; i < Beatmap.HitObjects.Count; i++) + { + OsuHitObject h = Beatmap.HitObjects[i]; + + if (DelayedMovements && i > 0) + { + OsuHitObject prev = Beatmap.HitObjects[i - 1]; + addDelayedMovements(h, prev); + } + + addHitObjectReplay(h); + } + + return Replay; + } + + private void addDelayedMovements(OsuHitObject h, OsuHitObject prev) + { + double endTime = (prev as IHasEndTime)?.EndTime ?? prev.StartTime; + + // 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)); + } + 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)); + } + 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)); + } + } + + private void addHitObjectReplay(OsuHitObject h) + { + // Default values for circles/sliders + Vector2 startPosition = h.Position; + EasingTypes easing = preferredEasing; + float spinnerDirection = -1; + + // The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position + // We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition. + // TODO: Shouldn't the spinner always spin in the same direction? + if (h is Spinner) + { + calcSpinnerStartPosAndDirection(Frames[Frames.Count - 1].Position, out startPosition, out spinnerDirection); + + Vector2 spinCentreOffset = SPINNER_CENTRE - Frames[Frames.Count - 1].Position; + + if (spinCentreOffset.Length > SPIN_RADIUS) + { + // If moving in from the outside, don't ease out (default eases out). This means auto will "start" spinning immediately after moving into position. + easing = EasingTypes.In; + } + } + + // Do some nice easing for cursor movements + if (Frames.Count > 0) + { + moveToHitObject(h.StartTime, startPosition, h.Radius, easing); + } + + // Add frames to click the hitobject + addHitObjectClickFrames(h, startPosition, spinnerDirection); + } + + #endregion + + #region Helper subroutines + + private static void calcSpinnerStartPosAndDirection(Vector2 prevPos, out Vector2 startPosition, out float spinnerDirection) + { + Vector2 spinCentreOffset = SPINNER_CENTRE - prevPos; + float distFromCentre = spinCentreOffset.Length; + float distToTangentPoint = (float)Math.Sqrt(distFromCentre * distFromCentre - SPIN_RADIUS * SPIN_RADIUS); + + if (distFromCentre > SPIN_RADIUS) + { + // Previous cursor position was outside spin circle, set startPosition to the tangent point. + + // Angle between centre offset and tangent point offset. + float angle = (float)Math.Asin(SPIN_RADIUS / distFromCentre); + + if (angle > 0) + { + spinnerDirection = -1; + } + else + { + spinnerDirection = 1; + } + + // Rotate by angle so it's parallel to tangent line + spinCentreOffset.X = spinCentreOffset.X * (float)Math.Cos(angle) - spinCentreOffset.Y * (float)Math.Sin(angle); + spinCentreOffset.Y = spinCentreOffset.X * (float)Math.Sin(angle) + spinCentreOffset.Y * (float)Math.Cos(angle); + + // Set length to distToTangentPoint + spinCentreOffset.Normalize(); + spinCentreOffset *= distToTangentPoint; + + // Move along the tangent line, now startPosition is at the tangent point. + startPosition = prevPos + spinCentreOffset; + } + else if (spinCentreOffset.Length > 0) + { + // Previous cursor position was inside spin circle, set startPosition to the nearest point on spin circle. + startPosition = SPINNER_CENTRE - spinCentreOffset * (SPIN_RADIUS / spinCentreOffset.Length); + spinnerDirection = 1; + } + else + { + // Degenerate case where cursor position is exactly at the centre of the spin circle. + startPosition = SPINNER_CENTRE + new Vector2(0, -SPIN_RADIUS); + spinnerDirection = 1; + } + } + + private void moveToHitObject(double targetTime, Vector2 targetPos, double hitObjectRadius, EasingTypes easing) + { + ReplayFrame lastFrame = Frames[Frames.Count - 1]; + + // Wait until Auto could "see and react" to the next note. + double waitTime = targetTime - Math.Max(0.0, DrawableOsuHitObject.TIME_PREEMPT - reactionTime); + if (waitTime > lastFrame.Time) + { + lastFrame = new ReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState); + AddFrameToReplay(lastFrame); + } + + Vector2 lastPosition = lastFrame.Position; + + double timeDifference = ApplyModsToTime(targetTime - lastFrame.Time); + + // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. + if (timeDifference > 0 && // Sanity checks + ((lastPosition - targetPos).Length > hitObjectRadius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough + timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. + { + // Perform eased movement + for (double time = lastFrame.Time + FrameDelay; time < targetTime; time += FrameDelay) + { + Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, targetTime, easing); + AddFrameToReplay(new ReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState)); + } + + buttonIndex = 0; + } + else + { + buttonIndex++; + } + } + + // Add frames to click the hitobject + private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection) + { + // Time to insert the first frame which clicks the object + // Here we mainly need to determine which button to use + ReplayButtonState button = buttonIndex % 2 == 0 ? ReplayButtonState.Left1 : ReplayButtonState.Right1; + + ReplayFrame startFrame = new ReplayFrame(h.StartTime, startPosition.X, startPosition.Y, button); + + // 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); + + // Decrement because we want the previous frame, not the next one + int index = FindInsertionIndex(startFrame) - 1; + + // If the previous frame has a button pressed, force alternation. + // If there are frames ahead, modify those to use the new button press. + // Do we have a previous frame? No need to check for < replay.Count since we decremented! + if (index >= 0) + { + ReplayFrame previousFrame = Frames[index]; + var previousButton = previousFrame.ButtonState; + + // If a button is already held, then we simply alternate + if (previousButton != ReplayButtonState.None) + { + Debug.Assert(previousButton != (ReplayButtonState.Left1 | ReplayButtonState.Right1), "Previous button state was not Left1 nor Right1 despite only using those two states."); + + // Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button. + if (previousButton == button) + { + button = (ReplayButtonState.Left1 | ReplayButtonState.Right1) & ~button; + startFrame.ButtonState = button; + } + + // We always follow the most recent slider / spinner, so remove any other frames that occur while it exists. + int endIndex = FindInsertionIndex(endFrame); + + if (index < Frames.Count - 1) + Frames.RemoveRange(index + 1, Math.Max(0, endIndex - (index + 1))); + + // After alternating we need to keep holding the other button in the future rather than the previous one. + for (int j = index + 1; j < Frames.Count; ++j) + { + // Don't affect frames which stop pressing a button! + if (j < Frames.Count - 1 || Frames[j].ButtonState == previousButton) + Frames[j].ButtonState = button; + } + } + } + + AddFrameToReplay(startFrame); + + // We add intermediate frames for spinning / following a slider here. + if (h is Spinner) + { + Spinner s = h as Spinner; + + Vector2 difference = startPosition - SPINNER_CENTRE; + + float radius = difference.Length; + float angle = radius == 0 ? 0 : (float)Math.Atan2(difference.Y, difference.X); + + double t; + + for (double j = h.StartTime + FrameDelay; j < s.EndTime; j += FrameDelay) + { + t = ApplyModsToTime(j - h.StartTime) * spinnerDirection; + + Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); + AddFrameToReplay(new ReplayFrame((int)j, pos.X, pos.Y, button)); + } + + t = ApplyModsToTime(s.EndTime - h.StartTime) * spinnerDirection; + Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); + + AddFrameToReplay(new ReplayFrame(s.EndTime, endPosition.X, endPosition.Y, button)); + + endFrame.MouseX = endPosition.X; + endFrame.MouseY = endPosition.Y; + } + else if (h is Slider) + { + Slider s = h as Slider; + + for (double j = FrameDelay; j < s.Duration; j += FrameDelay) + { + Vector2 pos = s.PositionAt(j / s.Duration); + AddFrameToReplay(new ReplayFrame(h.StartTime + j, pos.X, pos.Y, button)); + } + + AddFrameToReplay(new ReplayFrame(s.EndTime, s.EndPosition.X, s.EndPosition.Y, button)); + } + + // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! + if (Frames[Frames.Count - 1].Time <= endFrame.Time) + AddFrameToReplay(endFrame); + } + + #endregion + } +} diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs new file mode 100644 index 0000000000..65ed9530f2 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -0,0 +1,96 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Replays; +using osu.Game.Users; + +namespace osu.Game.Rulesets.Osu.Replays +{ + public abstract class OsuAutoGeneratorBase : AutoGenerator + { + #region Constants + + /// + /// Constants (for spinners). + /// + protected static readonly Vector2 SPINNER_CENTRE = new Vector2(256, 192); + protected const float SPIN_RADIUS = 50; + + /// + /// The time in ms between each ReplayFrame. + /// + protected readonly double FrameDelay; + + #endregion + + #region Construction / Initialisation + + protected Replay Replay; + protected List Frames => Replay.Frames; + + protected OsuAutoGeneratorBase(Beatmap beatmap) + : base(beatmap) + { + Replay = new Replay + { + User = new User + { + Username = @"Autoplay", + } + }; + + // We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps. + FrameDelay = ApplyModsToRate(1000.0 / 60.0); + } + + #endregion + + #region Utilities + protected double ApplyModsToTime(double v) => v; + protected double ApplyModsToRate(double v) => v; + + private class ReplayFrameComparer : IComparer + { + public int Compare(ReplayFrame f1, ReplayFrame f2) + { + if (f1 == null) throw new ArgumentNullException(nameof(f1)); + if (f2 == null) throw new ArgumentNullException(nameof(f2)); + + return f1.Time.CompareTo(f2.Time); + } + } + + private static readonly IComparer replay_frame_comparer = new ReplayFrameComparer(); + + protected int FindInsertionIndex(ReplayFrame frame) + { + int index = Frames.BinarySearch(frame, replay_frame_comparer); + + if (index < 0) + { + index = ~index; + } + else + { + // Go to the first index which is actually bigger + while (index < Frames.Count && frame.Time == Frames[index].Time) + { + ++index; + } + } + + return index; + } + + protected void AddFrameToReplay(ReplayFrame frame) => Frames.Insert(FindInsertionIndex(frame), frame); + + protected static Vector2 CirclePosition(double t, double radius) => new Vector2((float)(Math.Cos(t) * radius), (float)(Math.Sin(t) * radius)); + + #endregion + } +} diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index fcad0061e4..8974b1bcbd 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -69,7 +69,6 @@ - @@ -83,6 +82,8 @@ + + @@ -102,6 +103,9 @@ + + +