diff --git a/.vscode/launch.json b/.vscode/launch.json index b3b86da42f..f1083179b8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,34 +1,22 @@ { "version": "0.2.0", - "configurations": [ - { - "name": "VisualTests (debug)", + "configurations": [{ + "name": "osu! (VisualTests)", "windows": { "type": "clr" }, "type": "mono", "request": "launch", - "program": "${workspaceRoot}/osu.Desktop.VisualTests/bin/Debug/osu!.exe", + "program": "${workspaceRoot}/osu.Desktop/bin/Debug/osu!.exe", + "args": [ + "--tests" + ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", "runtimeExecutable": null, "env": {}, "console": "internalConsole" }, - { - "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": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f285ebde67..3db43ca9bb 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,41 +2,41 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", - "problemMatcher": "$msCompile", - "isShellCommand": true, "command": "msbuild", + "type": "shell", "suppressTaskName": true, - "showOutput": "silent", "args": [ "/property:GenerateFullPaths=true", - "/property:DebugType=portable" + "/property:DebugType=portable", + "/verbosity:minimal", + "/m" //parallel compiling support. ], - "windows": { - "args": [ - "/property:GenerateFullPaths=true", - "/property:DebugType=portable", - "/m" //parallel compiling support. doesn't work well with mono atm - ] - }, - "tasks": [ - { + "tasks": [{ "taskName": "Build (Debug)", - "isBuildCommand": true + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$msCompile" + ] }, { "taskName": "Build (Release)", "args": [ "/property:Configuration=Release" + ], + "problemMatcher": [ + "$msCompile" ] }, - { - "taskName": "Clean All", - "dependsOn": ["Clean (Debug)", "Clean (Release)"] - }, { "taskName": "Clean (Debug)", "args": [ "/target:Clean" + ], + "problemMatcher": [ + "$msCompile" ] }, { @@ -44,6 +44,19 @@ "args": [ "/target:Clean", "/property:Configuration=Release" + ], + "problemMatcher": [ + "$msCompile" + ] + }, + { + "taskName": "Clean All", + "dependsOn": [ + "Clean (Debug)", + "Clean (Release)" + ], + "problemMatcher": [ + "$msCompile" ] } ] diff --git a/appveyor.yml b/appveyor.yml index cc6dfb9c88..b26a895788 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,4 +20,4 @@ build: verbosity: minimal after_build: - cmd: inspectcode /o="inspectcodereport.xml" /caches-home="inspectcode" osu.sln - - cmd: NVika parsereport "inspectcodereport.xml" \ No newline at end of file + - cmd: NVika parsereport "inspectcodereport.xml" --treatwarningsaserrors \ No newline at end of file diff --git a/osu-framework b/osu-framework index c76d8b811b..ba70b8eaa9 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit c76d8b811b93d0c0862f342ebbab70a461006e13 +Subproject commit ba70b8eaa9b79d4248873d4399f3b9e918fc3c8f diff --git a/osu-resources b/osu-resources index 9f46a456dc..f6042e1cb3 160000 --- a/osu-resources +++ b/osu-resources @@ -1 +1 @@ -Subproject commit 9f46a456dc3a56dcbff09671a3f588b16a464106 +Subproject commit f6042e1cb37cfad6c879d0e1245f7880c7fcd5f5 diff --git a/osu.Desktop.Deploy/App.config b/osu.Desktop.Deploy/App.config index 45685a74a8..6711f9c54e 100644 --- a/osu.Desktop.Deploy/App.config +++ b/osu.Desktop.Deploy/App.config @@ -31,6 +31,10 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste + + + + \ No newline at end of file diff --git a/osu.Desktop.Deploy/Program.cs b/osu.Desktop.Deploy/Program.cs index 37776b329f..644987ce5a 100644 --- a/osu.Desktop.Deploy/Program.cs +++ b/osu.Desktop.Deploy/Program.cs @@ -17,8 +17,8 @@ namespace osu.Desktop.Deploy { internal static class Program { - private const string nuget_path = @"packages\NuGet.CommandLine.3.5.0\tools\NuGet.exe"; - private const string squirrel_path = @"packages\squirrel.windows.1.5.2\tools\Squirrel.exe"; + private const string nuget_path = @"packages\NuGet.CommandLine.4.1.0\tools\NuGet.exe"; + private const string squirrel_path = @"packages\squirrel.windows.1.7.5\tools\Squirrel.exe"; private const string msbuild_path = @"C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe"; public static string StagingFolder = ConfigurationManager.AppSettings["StagingFolder"]; diff --git a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj b/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj index 1f9726b573..c6474eae5a 100644 --- a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj +++ b/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj @@ -49,10 +49,6 @@ $(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.PatchApi.dll True - - $(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\ICSharpCode.SharpZipLib.dll - True - $(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll True @@ -73,15 +69,19 @@ $(SolutionDir)\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll - $(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\NuGet.Squirrel.dll + $(SolutionDir)\packages\squirrel.windows.1.7.5\lib\Net45\NuGet.Squirrel.dll + True + + + $(SolutionDir)\packages\SharpCompress.0.17.1\lib\net45\SharpCompress.dll True $(SolutionDir)\packages\Splat.2.0.0\lib\Net45\Splat.dll True - - $(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\Squirrel.dll + + $(SolutionDir)\packages\squirrel.windows.1.7.5\lib\Net45\Squirrel.dll True diff --git a/osu.Desktop.Deploy/packages.config b/osu.Desktop.Deploy/packages.config index 4878297be9..3c5ca9f9a3 100644 --- a/osu.Desktop.Deploy/packages.config +++ b/osu.Desktop.Deploy/packages.config @@ -7,7 +7,8 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste - + + - + \ No newline at end of file diff --git a/osu.Desktop.VisualTests/Beatmaps/TestWorkingBeatmap.cs b/osu.Desktop.Tests/Beatmaps/TestWorkingBeatmap.cs similarity index 91% rename from osu.Desktop.VisualTests/Beatmaps/TestWorkingBeatmap.cs rename to osu.Desktop.Tests/Beatmaps/TestWorkingBeatmap.cs index b45574b761..084cfab309 100644 --- a/osu.Desktop.VisualTests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Desktop.Tests/Beatmaps/TestWorkingBeatmap.cs @@ -5,7 +5,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; -namespace osu.Desktop.VisualTests.Beatmaps +namespace osu.Desktop.Tests.Beatmaps { public class TestWorkingBeatmap : WorkingBeatmap { diff --git a/osu.Desktop.VisualTests/Platform/TestStorage.cs b/osu.Desktop.Tests/Platform/TestStorage.cs similarity index 92% rename from osu.Desktop.VisualTests/Platform/TestStorage.cs rename to osu.Desktop.Tests/Platform/TestStorage.cs index f711ddac24..39e4d8049f 100644 --- a/osu.Desktop.VisualTests/Platform/TestStorage.cs +++ b/osu.Desktop.Tests/Platform/TestStorage.cs @@ -8,7 +8,7 @@ using SQLite.Net.Interop; using SQLite.Net.Platform.Generic; using SQLite.Net.Platform.Win32; -namespace osu.Desktop.VisualTests.Platform +namespace osu.Desktop.Tests.Platform { public class TestStorage : DesktopStorage { diff --git a/osu.Desktop.Tests/Visual/OsuTestCase.cs b/osu.Desktop.Tests/Visual/OsuTestCase.cs new file mode 100644 index 0000000000..e54f7dbeb5 --- /dev/null +++ b/osu.Desktop.Tests/Visual/OsuTestCase.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Desktop.Platform; +using osu.Framework.Testing; +using osu.Game; + +namespace osu.Desktop.Tests.Visual +{ + [TestFixture] + public abstract class OsuTestCase : TestCase + { + [Test] + public override void RunTest() + { + using (var host = new HeadlessGameHost()) + host.Run(new OsuTestCaseTestRunner(this)); + } + + public class OsuTestCaseTestRunner : OsuGameBase + { + private readonly OsuTestCase testCase; + + public OsuTestCaseTestRunner(OsuTestCase testCase) + { + this.testCase = testCase; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Add(new TestCaseTestRunner.TestRunner(testCase)); + } + } + } +} diff --git a/osu.Desktop.Tests/Visual/TestCaseBeatSyncedContainer.cs b/osu.Desktop.Tests/Visual/TestCaseBeatSyncedContainer.cs new file mode 100644 index 0000000000..130a034133 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseBeatSyncedContainer.cs @@ -0,0 +1,205 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Lists; +using osu.Framework.Timing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; +using OpenTK.Graphics; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseBeatSyncedContainer : OsuTestCase + { + public override string Description => @"Tests beat synced containers."; + + private readonly MusicController mc; + + public TestCaseBeatSyncedContainer() + { + Clock = new FramedClock(); + Clock.ProcessFrame(); + + Add(new BeatContainer + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }); + + Add(mc = new MusicController + { + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + mc.ToggleVisibility(); + } + + private class BeatContainer : BeatSyncedContainer + { + private const int flash_layer_heigth = 150; + + private readonly InfoString timingPointCount; + private readonly InfoString currentTimingPoint; + private readonly InfoString beatCount; + private readonly InfoString currentBeat; + private readonly InfoString beatsPerMinute; + private readonly InfoString adjustedBeatLength; + private readonly InfoString timeUntilNextBeat; + private readonly InfoString timeSinceLastBeat; + + private readonly Box flashLayer; + + public BeatContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Children = new Drawable[] + { + new Container + { + Name = @"Info Layer", + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Bottom = flash_layer_heigth }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(150), + }, + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + timingPointCount = new InfoString(@"Timing points amount"), + currentTimingPoint = new InfoString(@"Current timing point"), + beatCount = new InfoString(@"Beats amount (in the current timing point)"), + currentBeat = new InfoString(@"Current beat"), + beatsPerMinute = new InfoString(@"BPM"), + adjustedBeatLength = new InfoString(@"Adjusted beat length"), + timeUntilNextBeat = new InfoString(@"Time until next beat"), + timeSinceLastBeat = new InfoString(@"Time since last beat"), + } + } + } + }, + new Container + { + Name = @"Color indicator", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Height = flash_layer_heigth, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + flashLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Alpha = 0, + } + } + } + }; + + Beatmap.ValueChanged += delegate + { + timingPointCount.Value = 0; + currentTimingPoint.Value = 0; + beatCount.Value = 0; + currentBeat.Value = 0; + beatsPerMinute.Value = 0; + adjustedBeatLength.Value = 0; + timeUntilNextBeat.Value = 0; + timeSinceLastBeat.Value = 0; + }; + } + + private SortedList timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints; + private TimingControlPoint getNextTimingPoint(TimingControlPoint current) + { + if (timingPoints[timingPoints.Count - 1] == current) + return current; + + return timingPoints[timingPoints.IndexOf(current) + 1]; + } + + private int calculateBeatCount(TimingControlPoint current) + { + if (timingPoints.Count == 0) return 0; + + if (timingPoints[timingPoints.Count - 1] == current) + return (int)Math.Ceiling((Beatmap.Value.Track.Length - current.Time) / current.BeatLength); + + return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); + } + + protected override void Update() + { + base.Update(); + timeUntilNextBeat.Value = TimeUntilNextBeat; + timeSinceLastBeat.Value = TimeSinceLastBeat; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + timingPointCount.Value = timingPoints.Count; + currentTimingPoint.Value = timingPoints.IndexOf(timingPoint); + beatCount.Value = calculateBeatCount(timingPoint); + currentBeat.Value = beatIndex; + beatsPerMinute.Value = 60000 / timingPoint.BeatLength; + adjustedBeatLength.Value = timingPoint.BeatLength; + + flashLayer.FadeOutFromOne(timingPoint.BeatLength); + } + } + + private class InfoString : FillFlowContainer + { + private const int text_size = 20; + private const int margin = 7; + + private readonly OsuSpriteText valueText; + + public double Value + { + set { valueText.Text = $"{value:G}"; } + } + + public InfoString(string header) + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Horizontal; + Add(new OsuSpriteText { Text = header + @": ", TextSize = text_size }); + Add(valueText = new OsuSpriteText { TextSize = text_size }); + Margin = new MarginPadding(margin); + } + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetailArea.cs b/osu.Desktop.Tests/Visual/TestCaseBeatmapDetailArea.cs similarity index 71% rename from osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetailArea.cs rename to osu.Desktop.Tests/Visual/TestCaseBeatmapDetailArea.cs index e755924a15..23b4ece4ec 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetailArea.cs +++ b/osu.Desktop.Tests/Visual/TestCaseBeatmapDetailArea.cs @@ -1,21 +1,20 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; +using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Testing; using osu.Game.Screens.Select; +using OpenTK; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseBeatmapDetailArea : TestCase + [TestFixture] + internal class TestCaseBeatmapDetailArea : OsuTestCase { public override string Description => @"Beatmap details in song select"; - public override void Reset() + public TestCaseBeatmapDetailArea() { - base.Reset(); - Add(new BeatmapDetailArea { Anchor = Anchor.Centre, diff --git a/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs b/osu.Desktop.Tests/Visual/TestCaseBeatmapDetails.cs similarity index 84% rename from osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs rename to osu.Desktop.Tests/Visual/TestCaseBeatmapDetails.cs index 58cbad936a..11a15cf56f 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseBeatmapDetails.cs +++ b/osu.Desktop.Tests/Visual/TestCaseBeatmapDetails.cs @@ -1,24 +1,21 @@ // 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.Testing; -using osu.Game.Database; -using osu.Game.Screens.Select; using System.Linq; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseBeatmapDetails : TestCase + internal class TestCaseBeatmapDetails : OsuTestCase { public override string Description => "BeatmapDetails tab of BeatmapDetailArea"; - private BeatmapDetails details; + private readonly BeatmapDetails details; - public override void Reset() + public TestCaseBeatmapDetails() { - base.Reset(); - Add(details = new BeatmapDetails { RelativeSizeAxes = Axes.Both, @@ -41,7 +38,7 @@ namespace osu.Desktop.VisualTests.Tests StarDifficulty = 5.3f, Metrics = new BeatmapMetrics { - Ratings = Enumerable.Range(0,10), + Ratings = Enumerable.Range(0, 10), Fails = Enumerable.Range(lastRange, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(lastRange - 3, 100).Select(i => i % 12 - 6), }, diff --git a/osu.Desktop.VisualTests/Tests/TestCaseBeatmapOptionsOverlay.cs b/osu.Desktop.Tests/Visual/TestCaseBeatmapOptionsOverlay.cs similarity index 81% rename from osu.Desktop.VisualTests/Tests/TestCaseBeatmapOptionsOverlay.cs rename to osu.Desktop.Tests/Visual/TestCaseBeatmapOptionsOverlay.cs index 7c211227c6..3265f8ec76 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseBeatmapOptionsOverlay.cs +++ b/osu.Desktop.Tests/Visual/TestCaseBeatmapOptionsOverlay.cs @@ -1,22 +1,19 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Graphics; -using OpenTK.Input; -using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Screens.Select.Options; +using OpenTK.Graphics; +using OpenTK.Input; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseBeatmapOptionsOverlay : TestCase + internal class TestCaseBeatmapOptionsOverlay : OsuTestCase { public override string Description => @"Beatmap options in song select"; - public override void Reset() + public TestCaseBeatmapOptionsOverlay() { - base.Reset(); - var overlay = new BeatmapOptionsOverlay(); overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.fa_times_circle_o, Color4.Purple, null, Key.Number1); diff --git a/osu.Desktop.VisualTests/Tests/TestCaseBreadcrumbs.cs b/osu.Desktop.Tests/Visual/TestCaseBreadcrumbs.cs similarity index 81% rename from osu.Desktop.VisualTests/Tests/TestCaseBreadcrumbs.cs rename to osu.Desktop.Tests/Visual/TestCaseBreadcrumbs.cs index 658d2f92b1..10e0c784ab 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseBreadcrumbs.cs +++ b/osu.Desktop.Tests/Visual/TestCaseBreadcrumbs.cs @@ -1,20 +1,17 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseBreadcrumbs : TestCase + internal class TestCaseBreadcrumbs : OsuTestCase { public override string Description => @"breadcrumb > control"; - public override void Reset() + public TestCaseBreadcrumbs() { - base.Reset(); - BreadcrumbControl c; Add(c = new BreadcrumbControl { diff --git a/osu.Desktop.Tests/Visual/TestCaseCatcher.cs b/osu.Desktop.Tests/Visual/TestCaseCatcher.cs new file mode 100644 index 0000000000..3f57b5eeb9 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseCatcher.cs @@ -0,0 +1,35 @@ +// 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; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Catch.UI; +using OpenTK; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseCatcher : OsuTestCase + { + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + Children = new Drawable[] + { + new CatchInputManager(rulesets.GetRuleset(2)) + { + RelativeSizeAxes = Axes.Both, + Child = new CatcherArea + { + RelativePositionAxes = Axes.Both, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Size = new Vector2(1, 0.2f), + } + }, + }; + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseChatDisplay.cs b/osu.Desktop.Tests/Visual/TestCaseChatDisplay.cs similarity index 68% rename from osu.Desktop.VisualTests/Tests/TestCaseChatDisplay.cs rename to osu.Desktop.Tests/Visual/TestCaseChatDisplay.cs index 2663c952cf..ae051a1e40 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseChatDisplay.cs +++ b/osu.Desktop.Tests/Visual/TestCaseChatDisplay.cs @@ -1,20 +1,17 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Testing; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseChatDisplay : TestCase + internal class TestCaseChatDisplay : OsuTestCase { public override string Description => @"Testing chat api and overlay"; - public override void Reset() + public TestCaseChatDisplay() { - base.Reset(); - Add(new ChatOverlay { State = Visibility.Visible diff --git a/osu.Desktop.Tests/Visual/TestCaseContextMenu.cs b/osu.Desktop.Tests/Visual/TestCaseContextMenu.cs new file mode 100644 index 0000000000..f0894f794a --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseContextMenu.cs @@ -0,0 +1,97 @@ +// 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.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseContextMenu : OsuTestCase + { + public override string Description => @"Menu visible on right click"; + + private const int start_time = 0; + private const int duration = 1000; + + private readonly Container container; + + public TestCaseContextMenu() + { + Add(container = new MyContextMenuContainer + { + Size = new Vector2(200), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Green, + } + } + }); + + Add(new AnotherContextMenuContainer + { + Size = new Vector2(200), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Red, + } + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // Move box along a square trajectory + container.Loop(c => c + .MoveTo(new Vector2(0, 100), duration).Then() + .MoveTo(new Vector2(100, 100), duration).Then() + .MoveTo(new Vector2(100, 0), duration).Then() + .MoveTo(Vector2.Zero, duration) + ); + } + + private class MyContextMenuContainer : Container, IHasContextMenu + { + public ContextMenuItem[] ContextMenuItems => new ContextMenuItem[] + { + new OsuContextMenuItem(@"Some option"), + new OsuContextMenuItem(@"Highlighted option", MenuItemType.Highlighted), + new OsuContextMenuItem(@"Another option"), + new OsuContextMenuItem(@"Choose me please"), + new OsuContextMenuItem(@"And me too"), + new OsuContextMenuItem(@"Trying to fill"), + new OsuContextMenuItem(@"Destructive option", MenuItemType.Destructive), + }; + } + + private class AnotherContextMenuContainer : Container, IHasContextMenu + { + public ContextMenuItem[] ContextMenuItems => new ContextMenuItem[] + { + new OsuContextMenuItem(@"Simple option"), + new OsuContextMenuItem(@"Simple very very long option"), + new OsuContextMenuItem(@"Change width", MenuItemType.Highlighted) { Action = () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint) }, + new OsuContextMenuItem(@"Change height", MenuItemType.Highlighted) { Action = () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint) }, + new OsuContextMenuItem(@"Change width back", MenuItemType.Destructive) { Action = () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint) }, + new OsuContextMenuItem(@"Change height back", MenuItemType.Destructive) { Action = () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint) }, + }; + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseDialogOverlay.cs b/osu.Desktop.Tests/Visual/TestCaseDialogOverlay.cs similarity index 88% rename from osu.Desktop.VisualTests/Tests/TestCaseDialogOverlay.cs rename to osu.Desktop.Tests/Visual/TestCaseDialogOverlay.cs index 90e214c3c9..df15350453 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseDialogOverlay.cs +++ b/osu.Desktop.Tests/Visual/TestCaseDialogOverlay.cs @@ -1,22 +1,19 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseDialogOverlay : TestCase + internal class TestCaseDialogOverlay : OsuTestCase { public override string Description => @"Display dialogs"; - private DialogOverlay overlay; - - public override void Reset() + public TestCaseDialogOverlay() { - base.Reset(); + DialogOverlay overlay; Add(overlay = new DialogOverlay()); diff --git a/osu.Desktop.VisualTests/Tests/TestCaseDirect.cs b/osu.Desktop.Tests/Visual/TestCaseDirect.cs similarity index 66% rename from osu.Desktop.VisualTests/Tests/TestCaseDirect.cs rename to osu.Desktop.Tests/Visual/TestCaseDirect.cs index 4cda14559f..b78ea02767 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseDirect.cs +++ b/osu.Desktop.Tests/Visual/TestCaseDirect.cs @@ -3,22 +3,22 @@ using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Testing; -using osu.Game.Database; +using osu.Game.Beatmaps; using osu.Game.Overlays; +using osu.Game.Rulesets; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - public class TestCaseDirect : TestCase + public class TestCaseDirect : OsuTestCase { public override string Description => @"osu!direct overlay"; private DirectOverlay direct; - private RulesetDatabase rulesets; + private RulesetStore rulesets; - public override void Reset() + protected override void LoadComplete() { - base.Reset(); + base.LoadComplete(); Add(direct = new DirectOverlay()); newBeatmaps(); @@ -28,7 +28,7 @@ namespace osu.Desktop.VisualTests.Tests } [BackgroundDependencyLoader] - private void load(RulesetDatabase rulesets) + private void load(RulesetStore rulesets) { this.rulesets = rulesets; } @@ -48,6 +48,17 @@ namespace osu.Desktop.VisualTests.Tests Author = @"RLC", Source = @"", }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Card = @"https://assets.ppy.sh/beatmaps/578332/covers/card.jpg?1494591390", + Cover = @"https://assets.ppy.sh/beatmaps/578332/covers/cover.jpg?1494591390", + }, + Preview = @"https://b.ppy.sh/preview/578332.mp3", + PlayCount = 97, + FavouriteCount = 72, + }, Beatmaps = new List { new BeatmapInfo @@ -55,13 +66,6 @@ namespace osu.Desktop.VisualTests.Tests Ruleset = ruleset, StarDifficulty = 5.35f, Metadata = new BeatmapMetadata(), - OnlineInfo = new BeatmapOnlineInfo - { - Covers = new[] { @"https://assets.ppy.sh//beatmaps/578332/covers/cover.jpg?1494591390" }, - Preview = @"https://b.ppy.sh/preview/578332.mp3", - PlayCount = 97, - FavouriteCount = 72, - }, }, }, }, @@ -74,6 +78,17 @@ namespace osu.Desktop.VisualTests.Tests Author = @"Sotarks", Source = @"ぎんぎつね", }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Card = @"https://assets.ppy.sh/beatmaps/599627/covers/card.jpg?1494539318", + Cover = @"https://assets.ppy.sh/beatmaps/599627/covers/cover.jpg?1494539318", + }, + Preview = @"https//b.ppy.sh/preview/599627.mp3", + PlayCount = 3082, + FavouriteCount = 14, + }, Beatmaps = new List { new BeatmapInfo @@ -81,13 +96,6 @@ namespace osu.Desktop.VisualTests.Tests Ruleset = ruleset, StarDifficulty = 5.81f, Metadata = new BeatmapMetadata(), - OnlineInfo = new BeatmapOnlineInfo - { - Covers = new[] { @"https://assets.ppy.sh//beatmaps/599627/covers/cover.jpg?1494539318" }, - Preview = @"https//b.ppy.sh/preview/599627.mp3", - PlayCount = 3082, - FavouriteCount = 14, - }, }, }, }, @@ -100,6 +108,17 @@ namespace osu.Desktop.VisualTests.Tests Author = @"Cerulean Veyron", Source = @"", }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Card = @"https://assets.ppy.sh/beatmaps/513268/covers/card.jpg?1494502863", + Cover = @"https://assets.ppy.sh/beatmaps/513268/covers/cover.jpg?1494502863", + }, + Preview = @"https//b.ppy.sh/preview/513268.mp3", + PlayCount = 2762, + FavouriteCount = 15, + }, Beatmaps = new List { new BeatmapInfo @@ -107,13 +126,6 @@ namespace osu.Desktop.VisualTests.Tests Ruleset = ruleset, StarDifficulty = 0.9f, Metadata = new BeatmapMetadata(), - OnlineInfo = new BeatmapOnlineInfo - { - Covers = new[] { @"https://assets.ppy.sh//beatmaps/513268/covers/cover.jpg?1494502863" }, - Preview = @"https//b.ppy.sh/preview/513268.mp3", - PlayCount = 2762, - FavouriteCount = 15, - }, }, new BeatmapInfo { @@ -141,6 +153,17 @@ namespace osu.Desktop.VisualTests.Tests Author = @"[Kamiya]", Source = @"小林さんちのメイドラゴン", }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Card = @"https://assets.ppy.sh/beatmaps/586841/covers/card.jpg?1494052741", + Cover = @"https://assets.ppy.sh/beatmaps/586841/covers/cover.jpg?1494052741", + }, + Preview = @"https//b.ppy.sh/preview/586841.mp3", + PlayCount = 62317, + FavouriteCount = 161, + }, Beatmaps = new List { new BeatmapInfo @@ -148,13 +171,6 @@ namespace osu.Desktop.VisualTests.Tests Ruleset = ruleset, StarDifficulty = 1.26f, Metadata = new BeatmapMetadata(), - OnlineInfo = new BeatmapOnlineInfo - { - Covers = new[] { @"https://assets.ppy.sh//beatmaps/586841/covers/cover.jpg?1494052741" }, - Preview = @"https//b.ppy.sh/preview/586841.mp3", - PlayCount = 62317, - FavouriteCount = 161, - }, }, new BeatmapInfo { diff --git a/osu.Desktop.Tests/Visual/TestCaseDrawableRoom.cs b/osu.Desktop.Tests/Visual/TestCaseDrawableRoom.cs new file mode 100644 index 0000000000..04d551afe4 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseDrawableRoom.cs @@ -0,0 +1,132 @@ +// 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; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens.Multiplayer; +using osu.Game.Users; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseDrawableRoom : OsuTestCase + { + public override string Description => @"Select your favourite room"; + + private RulesetStore rulesets; + + protected override void LoadComplete() + { + base.LoadComplete(); + + DrawableRoom first; + Add(new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = 580f, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + first = new DrawableRoom(new Room + { + Name = { Value = @"Great Room Right Here" }, + Host = { Value = new User { Username = @"Naeferith", Id = 9492835, Country = new Country { FlagName = @"FR" } } }, + Status = { Value = new RoomStatusOpen() }, + Type = { Value = new GameTypeTeamVersus() }, + Beatmap = + { + Value = new BeatmapInfo + { + StarDifficulty = 4.65, + Ruleset = rulesets.GetRuleset(3), + Metadata = new BeatmapMetadata + { + Title = @"Critical Crystal", + Artist = @"Seiryu", + }, + BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh//beatmaps/376340/covers/cover.jpg?1456478455", + }, + }, + }, + }, + }, + Participants = + { + Value = new[] + { + new User { GlobalRank = 1355 }, + new User { GlobalRank = 8756 }, + }, + }, + }), + new DrawableRoom(new Room + { + Name = { Value = @"Relax It's The Weekend" }, + Host = { Value = new User { Username = @"peppy", Id = 2, Country = new Country { FlagName = @"AU" } } }, + Status = { Value = new RoomStatusPlaying() }, + Type = { Value = new GameTypeTagTeam() }, + Beatmap = + { + Value = new BeatmapInfo + { + StarDifficulty = 1.96, + Ruleset = rulesets.GetRuleset(0), + Metadata = new BeatmapMetadata + { + Title = @"Serendipity", + Artist = @"ZAQ", + }, + BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh//beatmaps/526839/covers/cover.jpg?1493815706", + }, + }, + }, + }, + }, + Participants = + { + Value = new[] + { + new User { GlobalRank = 578975 }, + new User { GlobalRank = 24554 }, + }, + }, + }), + } + }); + + AddStep(@"change title", () => first.Room.Name.Value = @"I Changed Name"); + AddStep(@"change host", () => first.Room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }); + AddStep(@"change status", () => first.Room.Status.Value = new RoomStatusPlaying()); + AddStep(@"change type", () => first.Room.Type.Value = new GameTypeVersus()); + AddStep(@"change beatmap", () => first.Room.Beatmap.Value = null); + AddStep(@"change participants", () => first.Room.Participants.Value = new[] + { + new User { GlobalRank = 1254 }, + new User { GlobalRank = 123189 }, + }); + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + this.rulesets = rulesets; + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseDrawings.cs b/osu.Desktop.Tests/Visual/TestCaseDrawings.cs similarity index 89% rename from osu.Desktop.VisualTests/Tests/TestCaseDrawings.cs rename to osu.Desktop.Tests/Visual/TestCaseDrawings.cs index ebc9930f93..81fd8cad03 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseDrawings.cs +++ b/osu.Desktop.Tests/Visual/TestCaseDrawings.cs @@ -2,20 +2,17 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using osu.Framework.Testing; using osu.Game.Screens.Tournament; using osu.Game.Screens.Tournament.Teams; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseDrawings : TestCase + internal class TestCaseDrawings : OsuTestCase { public override string Description => "Tournament drawings"; - public override void Reset() + public TestCaseDrawings() { - base.Reset(); - Add(new Drawings { TeamList = new TestTeamList(), diff --git a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs b/osu.Desktop.Tests/Visual/TestCaseGamefield.cs similarity index 77% rename from osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs rename to osu.Desktop.Tests/Visual/TestCaseGamefield.cs index e2cd2bf67b..73c61f5669 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseGamefield.cs +++ b/osu.Desktop.Tests/Visual/TestCaseGamefield.cs @@ -1,42 +1,45 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; +using System.Collections.Generic; +using osu.Desktop.Tests.Beatmaps; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.MathUtils; -using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; -using osu.Game.Database; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.UI; -using System.Collections.Generic; -using osu.Desktop.VisualTests.Beatmaps; -using osu.Framework.Allocation; -using osu.Game.Beatmaps.ControlPoints; +using OpenTK; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseGamefield : TestCase + internal class TestCaseGamefield : OsuTestCase { - private RulesetDatabase rulesets; + private RulesetStore rulesets; public override string Description => @"Showing hitobjects and what not."; [BackgroundDependencyLoader] - private void load(RulesetDatabase rulesets) + private void load(RulesetStore rulesets) { this.rulesets = rulesets; } - public override void Reset() + protected override void LoadComplete() { - base.Reset(); + base.LoadComplete(); List objects = new List(); @@ -76,7 +79,7 @@ namespace osu.Desktop.VisualTests.Tests ControlPointInfo = controlPointInfo }); - Add(new Drawable[] + AddRange(new Drawable[] { new Container { @@ -85,25 +88,25 @@ namespace osu.Desktop.VisualTests.Tests Clock = new FramedClock(), Children = new Drawable[] { - new OsuHitRenderer(beatmap, false) + new OsuRulesetContainer(new OsuRuleset(new RulesetInfo()), beatmap, false) { Scale = new Vector2(0.5f), Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft }, - new TaikoHitRenderer(beatmap, false) + new TaikoRulesetContainer(new TaikoRuleset(new RulesetInfo()),beatmap, false) { Scale = new Vector2(0.5f), Anchor = Anchor.TopRight, Origin = Anchor.TopRight }, - new CatchHitRenderer(beatmap, false) + new CatchRulesetContainer(new CatchRuleset(new RulesetInfo()),beatmap, false) { Scale = new Vector2(0.5f), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft }, - new ManiaHitRenderer(beatmap, false) + new ManiaRulesetContainer(new ManiaRuleset(new RulesetInfo()),beatmap, false) { Scale = new Vector2(0.5f), Anchor = Anchor.BottomRight, diff --git a/osu.Desktop.VisualTests/Tests/TestCaseGraph.cs b/osu.Desktop.Tests/Visual/TestCaseGraph.cs similarity index 81% rename from osu.Desktop.VisualTests/Tests/TestCaseGraph.cs rename to osu.Desktop.Tests/Visual/TestCaseGraph.cs index 7ac795f6f9..13342d1ff1 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseGraph.cs +++ b/osu.Desktop.Tests/Visual/TestCaseGraph.cs @@ -1,23 +1,20 @@ // 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.Graphics; -using osu.Framework.Testing; -using osu.Game.Graphics.UserInterface; using System.Linq; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using OpenTK; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseGraph : TestCase + internal class TestCaseGraph : OsuTestCase { public override string Description => "graph"; - private BarGraph graph; - - public override void Reset() + public TestCaseGraph() { - base.Reset(); + BarGraph graph; Children = new[] { @@ -30,7 +27,7 @@ namespace osu.Desktop.VisualTests.Tests }, }; - AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1,10).Select(i => (float)i)); + AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Select(i => (float)i)); AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).Select(i => (float)i)); AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().Select(i => (float)i)); AddStep("Bottom to top", () => graph.Direction = BarDirection.BottomToTop); diff --git a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs b/osu.Desktop.Tests/Visual/TestCaseHitObjects.cs similarity index 64% rename from osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs rename to osu.Desktop.Tests/Visual/TestCaseHitObjects.cs index 8c913ae95e..13e05d6477 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseHitObjects.cs +++ b/osu.Desktop.Tests/Visual/TestCaseHitObjects.cs @@ -1,43 +1,62 @@ // 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 osu.Framework.Configuration; +using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; -using System.Collections.Generic; +using OpenTK; +using osu.Game.Rulesets.Osu; +using osu.Framework.Allocation; +using osu.Game.Rulesets; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseHitObjects : TestCase + internal class TestCaseHitObjects : OsuTestCase { - private readonly FramedClock framedClock; + private FramedClock framedClock; private bool auto; - public TestCaseHitObjects() + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) { var rateAdjustClock = new StopwatchClock(true); framedClock = new FramedClock(rateAdjustClock); - playbackSpeed.ValueChanged += delegate { rateAdjustClock.Rate = playbackSpeed.Value; }; + + AddStep(@"circles", () => loadHitobjects(HitObjectType.Circle)); + AddStep(@"slider", () => loadHitobjects(HitObjectType.Slider)); + AddStep(@"spinner", () => loadHitobjects(HitObjectType.Spinner)); + + AddToggleStep("Auto", state => { auto = state; loadHitobjects(mode); }); + AddSliderStep("Playback speed", 0.0, 2.0, 0.5, v => rateAdjustClock.Rate = v); + + framedClock.ProcessFrame(); + + var clockAdjustContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Clock = framedClock, + Children = new[] + { + playfieldContainer = new OsuInputManager(rulesets.GetRuleset(0)) { RelativeSizeAxes = Axes.Both }, + approachContainer = new Container { RelativeSizeAxes = Axes.Both } + } + }; + + Add(clockAdjustContainer); } private HitObjectType mode = HitObjectType.Slider; - private readonly BindableNumber playbackSpeed = new BindableDouble(0.5) { MinValue = 0, MaxValue = 1 }; private Container playfieldContainer; private Container approachContainer; - private void load(HitObjectType mode) + private void loadHitobjects(HitObjectType mode) { this.mode = mode; @@ -83,54 +102,6 @@ namespace osu.Desktop.VisualTests.Tests } } - public override void Reset() - { - base.Reset(); - - playbackSpeed.TriggerChange(); - - AddStep(@"circles", () => load(HitObjectType.Circle)); - AddStep(@"slider", () => load(HitObjectType.Slider)); - AddStep(@"spinner", () => load(HitObjectType.Spinner)); - - AddToggleStep(@"auto", state => { auto = state; load(mode); }); - - BasicSliderBar sliderBar; - Add(new Container - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new SpriteText { Text = "Playback Speed" }, - sliderBar = new BasicSliderBar - { - Width = 150, - Height = 10, - SelectionColor = Color4.Orange, - } - } - }); - - sliderBar.Current.BindTo(playbackSpeed); - - framedClock.ProcessFrame(); - - var clockAdjustContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Clock = framedClock, - Children = new[] - { - playfieldContainer = new Container { RelativeSizeAxes = Axes.Both }, - approachContainer = new Container { RelativeSizeAxes = Axes.Both } - } - }; - - Add(clockAdjustContainer); - } - private int depth; private void add(DrawableOsuHitObject h) diff --git a/osu.Desktop.Tests/Visual/TestCaseKeyConfiguration.cs b/osu.Desktop.Tests/Visual/TestCaseKeyConfiguration.cs new file mode 100644 index 0000000000..cab285c72e --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseKeyConfiguration.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Overlays; + +namespace osu.Desktop.Tests.Visual +{ + public class TestCaseKeyConfiguration : OsuTestCase + { + private readonly KeyBindingOverlay overlay; + + public override string Description => @"Key configuration"; + + public TestCaseKeyConfiguration() + { + Child = overlay = new KeyBindingOverlay(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + overlay.Show(); + } + } +} diff --git a/osu.Desktop.Tests/Visual/TestCaseKeyCounter.cs b/osu.Desktop.Tests/Visual/TestCaseKeyCounter.cs new file mode 100644 index 0000000000..ffe37c83a6 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseKeyCounter.cs @@ -0,0 +1,41 @@ +// 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.MathUtils; +using osu.Game.Screens.Play; +using OpenTK.Input; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseKeyCounter : OsuTestCase + { + public override string Description => @"Tests key counter"; + + public TestCaseKeyCounter() + { + KeyCounterCollection kc = new KeyCounterCollection + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + IsCounting = true, + Children = new KeyCounter[] + { + new KeyCounterKeyboard(Key.Z), + new KeyCounterKeyboard(Key.X), + new KeyCounterMouse(MouseButton.Left), + new KeyCounterMouse(MouseButton.Right), + }, + }; + + AddStep("Add random", () => + { + Key key = (Key)((int)Key.A + RNG.Next(26)); + kc.Add(new KeyCounterKeyboard(key)); + }); + AddSliderStep("Fade time", 0, 200, 50, v => kc.FadeTime = v); + + Add(kc); + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseLeaderboard.cs b/osu.Desktop.Tests/Visual/TestCaseLeaderboard.cs similarity index 94% rename from osu.Desktop.VisualTests/Tests/TestCaseLeaderboard.cs rename to osu.Desktop.Tests/Visual/TestCaseLeaderboard.cs index 39010baf91..5f8ee8795c 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseLeaderboard.cs +++ b/osu.Desktop.Tests/Visual/TestCaseLeaderboard.cs @@ -1,22 +1,21 @@ // 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.Graphics; -using osu.Framework.Testing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; +using OpenTK; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseLeaderboard : TestCase + internal class TestCaseLeaderboard : OsuTestCase { public override string Description => @"From song select"; - private Leaderboard leaderboard; + private readonly Leaderboard leaderboard; private void newScores() { @@ -207,10 +206,8 @@ namespace osu.Desktop.VisualTests.Tests leaderboard.Scores = scores; } - public override void Reset() + public TestCaseLeaderboard() { - base.Reset(); - Add(leaderboard = new Leaderboard { Origin = Anchor.Centre, diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaHitObjects.cs b/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs similarity index 86% rename from osu.Desktop.VisualTests/Tests/TestCaseManiaHitObjects.cs rename to osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs index 3113b63db1..7dcc48c778 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseManiaHitObjects.cs +++ b/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs @@ -3,20 +3,17 @@ 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; +using OpenTK.Graphics; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseManiaHitObjects : TestCase + internal class TestCaseManiaHitObjects : OsuTestCase { - public override void Reset() + public TestCaseManiaHitObjects() { - base.Reset(); - Add(new FillFlowContainer { Anchor = Anchor.Centre, @@ -40,7 +37,7 @@ namespace osu.Desktop.VisualTests.Tests { Name = "Timing section", RelativeSizeAxes = Axes.Both, - RelativeCoordinateSpace = new Vector2(1, 10000), + RelativeChildSize = new Vector2(1, 10000), Children = new[] { new DrawableNote(new Note { StartTime = 5000 }) { AccentColour = Color4.Red }, @@ -62,7 +59,7 @@ namespace osu.Desktop.VisualTests.Tests { Name = "Timing section", RelativeSizeAxes = Axes.Both, - RelativeCoordinateSpace = new Vector2(1, 10000), + RelativeChildSize = new Vector2(1, 10000), Children = new[] { new DrawableHoldNote(new HoldNote diff --git a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs b/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs similarity index 61% rename from osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs rename to osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs index 95287c3199..ed0e5d81e9 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseManiaPlayfield.cs +++ b/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs @@ -1,36 +1,35 @@ // 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 OpenTK; -using osu.Game.Rulesets.Mania.Objects.Drawables; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.Timing; +using System.Linq; using osu.Framework.Configuration; -using OpenTK.Input; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Input; using osu.Framework.Timing; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Timing; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Timing; +using OpenTK; +using OpenTK.Input; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseManiaPlayfield : TestCase + internal class TestCaseManiaPlayfield : OsuTestCase { public override string Description => @"Mania playfield"; protected override double TimePerAction => 200; - public override void Reset() + public TestCaseManiaPlayfield() { - base.Reset(); - Action createPlayfield = (cols, pos) => { Clear(); - Add(new ManiaPlayfield(cols, new List()) + Add(new ManiaPlayfield(cols) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -39,37 +38,22 @@ namespace osu.Desktop.VisualTests.Tests }); }; - Action createPlayfieldWithNotes = (cols, pos) => + const double start_time = 500; + const double duration = 500; + + Func createTimingChange = (time, gravity) => new ManiaSpeedAdjustmentContainer(new MultiplierControlPoint(time) + { + TimingPoint = { BeatLength = 1000 } + }, gravity ? ScrollingAlgorithm.Gravity : ScrollingAlgorithm.Basic); + + Action createPlayfieldWithNotes = gravity => { Clear(); - ManiaPlayfield playField; - Add(playField = new ManiaPlayfield(cols, new List { new TimingChange { 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 - })); - } - }; - - Action createPlayfieldWithNotesAcceptingInput = () => - { - Clear(); - - var rateAdjustClock = new StopwatchClock(true) { Rate = 0.5 }; + var rateAdjustClock = new StopwatchClock(true) { Rate = 1 }; ManiaPlayfield playField; - Add(playField = new ManiaPlayfield(4, new List { new TimingChange { BeatLength = 200 } }) + Add(playField = new ManiaPlayfield(4) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -77,14 +61,23 @@ namespace osu.Desktop.VisualTests.Tests Clock = new FramedClock(rateAdjustClock) }); - for (int t = 1000; t <= 2000; t += 100) + if (!gravity) + playField.Columns.ForEach(c => c.Add(createTimingChange(0, false))); + + for (double t = start_time; t <= start_time + duration; t += 100) { + if (gravity) + playField.Columns.ElementAt(0).Add(createTimingChange(t, true)); + playField.Add(new DrawableNote(new Note { StartTime = t, Column = 0 }, new Bindable(Key.D))); + if (gravity) + playField.Columns.ElementAt(3).Add(createTimingChange(t, true)); + playField.Add(new DrawableNote(new Note { StartTime = t, @@ -92,17 +85,23 @@ namespace osu.Desktop.VisualTests.Tests }, new Bindable(Key.K))); } - playField.Add(new DrawableHoldNote(new HoldNote - { - StartTime = 1000, - Duration = 1000, - Column = 1 - }, new Bindable(Key.F))); + if (gravity) + playField.Columns.ElementAt(1).Add(createTimingChange(start_time, true)); playField.Add(new DrawableHoldNote(new HoldNote { - StartTime = 1000, - Duration = 1000, + StartTime = start_time, + Duration = duration, + Column = 1 + }, new Bindable(Key.F))); + + if (gravity) + playField.Columns.ElementAt(2).Add(createTimingChange(start_time, true)); + + playField.Add(new DrawableHoldNote(new HoldNote + { + StartTime = start_time, + Duration = duration, Column = 2 }, new Bindable(Key.J))); }; @@ -116,16 +115,11 @@ namespace osu.Desktop.VisualTests.Tests 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 with input", () => createPlayfieldWithNotes(false)); + AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction)); - 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); - - AddStep("Notes with input", () => createPlayfieldWithNotesAcceptingInput()); + AddStep("Notes with gravity", () => createPlayfieldWithNotes(true)); + AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction)); } private void triggerKeyDown(Column column) diff --git a/osu.Desktop.Tests/Visual/TestCaseMedalOverlay.cs b/osu.Desktop.Tests/Visual/TestCaseMedalOverlay.cs new file mode 100644 index 0000000000..747f2190b0 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseMedalOverlay.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Overlays; +using osu.Game.Users; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseMedalOverlay : OsuTestCase + { + public override string Description => @"medal get!"; + + public TestCaseMedalOverlay() + { + AddStep(@"display", () => + { + LoadComponentAsync(new MedalOverlay(new Medal + { + Name = @"Animations", + InternalName = @"all-intro-doubletime", + Description = @"More complex than you think.", + }), Add); + }); + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseMenuButtonSystem.cs b/osu.Desktop.Tests/Visual/TestCaseMenuButtonSystem.cs similarity index 59% rename from osu.Desktop.VisualTests/Tests/TestCaseMenuButtonSystem.cs rename to osu.Desktop.Tests/Visual/TestCaseMenuButtonSystem.cs index ddb62598cf..3c7ee343bb 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseMenuButtonSystem.cs +++ b/osu.Desktop.Tests/Visual/TestCaseMenuButtonSystem.cs @@ -1,25 +1,22 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Testing; using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Screens.Menu; using OpenTK.Graphics; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseMenuButtonSystem : TestCase + internal class TestCaseMenuButtonSystem : OsuTestCase { public override string Description => @"Main menu button system"; - public override void Reset() + public TestCaseMenuButtonSystem() { - base.Reset(); - Add(new Box { - ColourInfo = ColourInfo.GradientVertical(Color4.Gray, Color4.WhiteSmoke), + Colour = ColourInfo.GradientVertical(Color4.Gray, Color4.WhiteSmoke), RelativeSizeAxes = Framework.Graphics.Axes.Both, }); Add(new ButtonSystem()); diff --git a/osu.Desktop.VisualTests/Tests/TestCaseMenuOverlays.cs b/osu.Desktop.Tests/Visual/TestCaseMenuOverlays.cs similarity index 71% rename from osu.Desktop.VisualTests/Tests/TestCaseMenuOverlays.cs rename to osu.Desktop.Tests/Visual/TestCaseMenuOverlays.cs index 23fe8f16db..1f4ad9d3da 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseMenuOverlays.cs +++ b/osu.Desktop.Tests/Visual/TestCaseMenuOverlays.cs @@ -3,24 +3,20 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; -using osu.Framework.Testing; using osu.Game.Screens.Play; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseMenuOverlays : TestCase + internal class TestCaseMenuOverlays : OsuTestCase { public override string Description => @"Tests pause and fail overlays"; - private PauseContainer.PauseOverlay pauseOverlay; - private FailOverlay failOverlay; - private int retryCount; - - public override void Reset() + public TestCaseMenuOverlays() { - base.Reset(); + FailOverlay failOverlay; + PauseContainer.PauseOverlay pauseOverlay; - retryCount = 0; + var retryCount = 0; Add(pauseOverlay = new PauseContainer.PauseOverlay { @@ -34,14 +30,16 @@ namespace osu.Desktop.VisualTests.Tests OnQuit = () => Logger.Log(@"Quit"), }); - AddStep(@"Pause", delegate { - if(failOverlay.State == Visibility.Visible) + AddStep(@"Pause", delegate + { + if (failOverlay.State == Visibility.Visible) { failOverlay.Hide(); } pauseOverlay.Show(); }); - AddStep("Fail", delegate { + AddStep("Fail", delegate + { if (pauseOverlay.State == Visibility.Visible) { pauseOverlay.Hide(); diff --git a/osu.Desktop.VisualTests/Tests/TestCaseMods.cs b/osu.Desktop.Tests/Visual/TestCaseMods.cs similarity index 79% rename from osu.Desktop.VisualTests/Tests/TestCaseMods.cs rename to osu.Desktop.Tests/Visual/TestCaseMods.cs index 3f3a9d82f5..f48030067d 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseMods.cs +++ b/osu.Desktop.Tests/Visual/TestCaseMods.cs @@ -4,32 +4,31 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays.Mods; -using osu.Framework.Testing; -using osu.Game.Database; +using osu.Game.Rulesets; using osu.Game.Screens.Play.HUD; using OpenTK; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseMods : TestCase + internal class TestCaseMods : OsuTestCase { public override string Description => @"Mod select overlay and in-game display"; private ModSelectOverlay modSelect; private ModDisplay modDisplay; - private RulesetDatabase rulesets; + private RulesetStore rulesets; [BackgroundDependencyLoader] - private void load(RulesetDatabase rulesets) + private void load(RulesetStore rulesets) { this.rulesets = rulesets; } - public override void Reset() + protected override void LoadComplete() { - base.Reset(); + base.LoadComplete(); Add(modSelect = new ModSelectOverlay { diff --git a/osu.Desktop.VisualTests/Tests/TestCaseMusicController.cs b/osu.Desktop.Tests/Visual/TestCaseMusicController.cs similarity index 57% rename from osu.Desktop.VisualTests/Tests/TestCaseMusicController.cs rename to osu.Desktop.Tests/Visual/TestCaseMusicController.cs index 5665bf859a..8d71527a21 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseMusicController.cs +++ b/osu.Desktop.Tests/Visual/TestCaseMusicController.cs @@ -1,30 +1,28 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Testing; +using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Framework.Timing; -using osu.Game.Overlays; using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; +using osu.Game; +using osu.Game.Beatmaps; +using osu.Game.Overlays; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseMusicController : TestCase + internal class TestCaseMusicController : OsuTestCase { public override string Description => @"Tests music controller ui."; - private MusicController mc; + private readonly Bindable beatmapBacking = new Bindable(); public TestCaseMusicController() { Clock = new FramedClock(); - } - public override void Reset() - { - base.Reset(); - Clock.ProcessFrame(); - mc = new MusicController + var mc = new MusicController { Origin = Anchor.Centre, Anchor = Anchor.Centre @@ -33,6 +31,13 @@ namespace osu.Desktop.VisualTests.Tests AddToggleStep(@"toggle visibility", state => mc.State = state ? Visibility.Visible : Visibility.Hidden); AddStep(@"show", () => mc.State = Visibility.Visible); + AddToggleStep(@"toggle beatmap lock", state => beatmapBacking.Disabled = state); + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase game) + { + beatmapBacking.BindTo(game.Beatmap); } } } diff --git a/osu.Desktop.VisualTests/Tests/TestCaseNotificationManager.cs b/osu.Desktop.Tests/Visual/TestCaseNotificationOverlay.cs similarity index 84% rename from osu.Desktop.VisualTests/Tests/TestCaseNotificationManager.cs rename to osu.Desktop.Tests/Visual/TestCaseNotificationOverlay.cs index 8972040b06..1e46fbda35 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseNotificationManager.cs +++ b/osu.Desktop.Tests/Visual/TestCaseNotificationOverlay.cs @@ -2,29 +2,28 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Testing; +using osu.Framework.Graphics.Containers; using osu.Framework.MathUtils; using osu.Game.Overlays; -using System.Linq; using osu.Game.Overlays.Notifications; -using osu.Framework.Graphics.Containers; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseNotificationManager : TestCase + [TestFixture] + internal class TestCaseNotificationOverlay : OsuTestCase { public override string Description => @"I handle notifications"; - private NotificationManager manager; + private readonly NotificationOverlay manager; - public override void Reset() + public TestCaseNotificationOverlay() { - base.Reset(); - progressingNotifications.Clear(); - Content.Add(manager = new NotificationManager + Content.Add(manager = new NotificationOverlay { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -58,10 +57,7 @@ namespace osu.Desktop.VisualTests.Tests } if (remaining > 0) - { - Delay(80); - Schedule(() => sendBarrage(remaining - 1)); - } + Scheduler.AddDelayed(() => sendBarrage(remaining - 1), 80); } protected override void Update() @@ -72,7 +68,7 @@ namespace osu.Desktop.VisualTests.Tests while (progressingNotifications.Count(n => n.State == ProgressNotificationState.Active) < 3) { - var p = progressingNotifications.FirstOrDefault(n => n.IsLoaded && n.State == ProgressNotificationState.Queued); + var p = progressingNotifications.FirstOrDefault(n => n.IsAlive && n.State == ProgressNotificationState.Queued); if (p == null) break; diff --git a/osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs b/osu.Desktop.Tests/Visual/TestCaseOnScreenDisplay.cs similarity index 83% rename from osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs rename to osu.Desktop.Tests/Visual/TestCaseOnScreenDisplay.cs index 3cefb8a3d2..0b7a822e1d 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseOnScreenDisplay.cs +++ b/osu.Desktop.Tests/Visual/TestCaseOnScreenDisplay.cs @@ -3,21 +3,20 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; -using osu.Framework.Testing; using osu.Game.Overlays; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseOnScreenDisplay : TestCase + internal class TestCaseOnScreenDisplay : OsuTestCase { private FrameworkConfigManager config; private Bindable frameSyncMode; public override string Description => @"Make it easier to see setting changes"; - public override void Reset() + protected override void LoadComplete() { - base.Reset(); + base.LoadComplete(); Add(new OnScreenDisplay()); diff --git a/osu.Desktop.VisualTests/Tests/TestCasePlaySongSelect.cs b/osu.Desktop.Tests/Visual/TestCasePlaySongSelect.cs similarity index 73% rename from osu.Desktop.VisualTests/Tests/TestCasePlaySongSelect.cs rename to osu.Desktop.Tests/Visual/TestCasePlaySongSelect.cs index 35eb6d0ff9..379100b543 100644 --- a/osu.Desktop.VisualTests/Tests/TestCasePlaySongSelect.cs +++ b/osu.Desktop.Tests/Visual/TestCasePlaySongSelect.cs @@ -2,43 +2,40 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using osu.Desktop.VisualTests.Platform; -using osu.Framework.Testing; +using osu.Desktop.Tests.Platform; using osu.Framework.MathUtils; +using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Rulesets; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Filter; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCasePlaySongSelect : TestCase + internal class TestCasePlaySongSelect : OsuTestCase { - private BeatmapDatabase db; - private TestStorage storage; - private PlaySongSelect songSelect; + private readonly BeatmapManager manager; public override string Description => @"with fake data"; - private RulesetDatabase rulesets; + private readonly RulesetStore rulesets; - public override void Reset() + public TestCasePlaySongSelect() { - base.Reset(); - if (db == null) + PlaySongSelect songSelect; + + if (manager == null) { - storage = new TestStorage(@"TestCasePlaySongSelect"); + var storage = new TestStorage(@"TestCasePlaySongSelect"); var backingDatabase = storage.GetDatabase(@"client"); + backingDatabase.CreateTable(); - rulesets = new RulesetDatabase(storage, backingDatabase); - db = new BeatmapDatabase(storage, backingDatabase, rulesets); - - var sets = new List(); + rulesets = new RulesetStore(backingDatabase); + manager = new BeatmapManager(storage, null, backingDatabase, rulesets); for (int i = 0; i < 100; i += 10) - sets.Add(createTestBeatmapSet(i)); - - db.Import(sets); + manager.Import(createTestBeatmapSet(i)); } Add(songSelect = new PlaySongSelect()); @@ -49,21 +46,12 @@ namespace osu.Desktop.VisualTests.Tests AddStep(@"Sort by Difficulty", delegate { songSelect.FilterControl.Sort = SortMode.Difficulty; }); } - //protected override void Dispose(bool isDisposing) - //{ - // if (oldDb != null) - // db = null; - - // base.Dispose(isDisposing); - //} - private BeatmapSetInfo createTestBeatmapSet(int i) { return new BeatmapSetInfo { OnlineBeatmapSetID = 1234 + i, Hash = "d8e8fca2dc0f896fd7cb4cb0031ba249", - Path = string.Empty, Metadata = new BeatmapMetadata { OnlineBeatmapSetID = 1234 + i, diff --git a/osu.Desktop.Tests/Visual/TestCasePlayer.cs b/osu.Desktop.Tests/Visual/TestCasePlayer.cs new file mode 100644 index 0000000000..f5d02d3f2b --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCasePlayer.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Desktop.Tests.Beatmaps; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Play; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCasePlayer : OsuTestCase + { + protected Player Player; + private RulesetStore rulesets; + + public override string Description => @"Showing everything to play the game."; + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + this.rulesets = rulesets; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + var objects = new List(); + + int time = 1500; + for (int i = 0; i < 50; i++) + { + objects.Add(new HitCircle + { + StartTime = time, + Position = new Vector2(i % 4 == 0 || i % 4 == 2 ? 0 : OsuPlayfield.BASE_SIZE.X, + i % 4 < 2 ? 0 : OsuPlayfield.BASE_SIZE.Y), + NewCombo = i % 4 == 0 + }); + + time += 500; + } + + Beatmap b = new Beatmap + { + HitObjects = objects, + BeatmapInfo = new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(), + Ruleset = rulesets.Query().First(), + Metadata = new BeatmapMetadata + { + Artist = @"Unknown", + Title = @"Sample Beatmap", + Author = @"peppy", + } + } + }; + + WorkingBeatmap beatmap = new TestWorkingBeatmap(b); + + Add(new Box + { + RelativeSizeAxes = Framework.Graphics.Axes.Both, + Colour = Color4.Black, + }); + + Add(Player = CreatePlayer(beatmap)); + } + + protected virtual Player CreatePlayer(WorkingBeatmap beatmap) + { + return new Player + { + InitialBeatmap = beatmap + }; + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs b/osu.Desktop.Tests/Visual/TestCaseReplay.cs similarity index 90% rename from osu.Desktop.VisualTests/Tests/TestCaseReplay.cs rename to osu.Desktop.Tests/Visual/TestCaseReplay.cs index e00a912278..9b2f59e444 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseReplay.cs +++ b/osu.Desktop.Tests/Visual/TestCaseReplay.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Play; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { internal class TestCaseReplay : TestCasePlayer { diff --git a/osu.Desktop.Tests/Visual/TestCaseReplaySettingsOverlay.cs b/osu.Desktop.Tests/Visual/TestCaseReplaySettingsOverlay.cs new file mode 100644 index 0000000000..ab865dcab0 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseReplaySettingsOverlay.cs @@ -0,0 +1,52 @@ +// 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.Game.Graphics.UserInterface; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.ReplaySettings; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseReplaySettingsOverlay : OsuTestCase + { + public override string Description => @"Settings visible in replay/auto"; + + public TestCaseReplaySettingsOverlay() + { + ExampleContainer container; + + Add(new ReplaySettingsOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }); + + Add(container = new ExampleContainer()); + + AddStep(@"Add button", () => container.Add(new OsuButton + { + RelativeSizeAxes = Axes.X, + Text = @"Button", + })); + + AddStep(@"Add checkbox", () => container.Add(new ReplayCheckbox + { + LabelText = "Checkbox", + })); + + AddStep(@"Add textbox", () => container.Add(new FocusedTextBox + { + RelativeSizeAxes = Axes.X, + Height = 30, + PlaceholderText = "Textbox", + HoldFocus = false, + })); + } + + private class ExampleContainer : ReplayGroup + { + protected override string Title => @"example"; + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseResults.cs b/osu.Desktop.Tests/Visual/TestCaseResults.cs similarity index 64% rename from osu.Desktop.VisualTests/Tests/TestCaseResults.cs rename to osu.Desktop.Tests/Visual/TestCaseResults.cs index f8c93e9a73..a0622b302a 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseResults.cs +++ b/osu.Desktop.Tests/Visual/TestCaseResults.cs @@ -3,44 +3,39 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Users; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseResults : TestCase + internal class TestCaseResults : OsuTestCase { - private BeatmapDatabase db; + private BeatmapManager beatmaps; public override string Description => @"Results after playing."; [BackgroundDependencyLoader] - private void load(BeatmapDatabase db) + private void load(BeatmapManager beatmaps) { - this.db = db; + this.beatmaps = beatmaps; } private WorkingBeatmap beatmap; - public override void Reset() + protected override void LoadComplete() { - base.Reset(); + base.LoadComplete(); if (beatmap == null) { - var beatmapInfo = db.Query().FirstOrDefault(b => b.RulesetID == 0); + var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); if (beatmapInfo != null) - beatmap = db.GetWorkingBeatmap(beatmapInfo); + beatmap = beatmaps.GetWorkingBeatmap(beatmapInfo); } - base.Reset(); - Add(new Results(new Score { TotalScore = 2845370, @@ -48,7 +43,7 @@ namespace osu.Desktop.VisualTests.Tests MaxCombo = 123, Rank = ScoreRank.A, Date = DateTimeOffset.Now, - Statistics = new Dictionary() + Statistics = new Dictionary { { "300", 50 }, { "100", 20 }, @@ -61,7 +56,7 @@ namespace osu.Desktop.VisualTests.Tests } }) { - Beatmap = beatmap + InitialBeatmap = beatmap }); } } diff --git a/osu.Desktop.Tests/Visual/TestCaseRoomInspector.cs b/osu.Desktop.Tests/Visual/TestCaseRoomInspector.cs new file mode 100644 index 0000000000..db557baed2 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseRoomInspector.cs @@ -0,0 +1,143 @@ +// 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; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens.Multiplayer; +using osu.Game.Users; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseRoomInspector : OsuTestCase + { + public override string Description => @"from the multiplayer lobby"; + + private RulesetStore rulesets; + + protected override void LoadComplete() + { + base.LoadComplete(); + + var room = new Room + { + Name = { Value = @"My Awesome Room" }, + Host = { Value = new User { Username = @"flyte", Id = 3103765, Country = new Country { FlagName = @"JP" } } }, + Status = { Value = new RoomStatusOpen() }, + Type = { Value = new GameTypeTeamVersus() }, + Beatmap = + { + Value = new BeatmapInfo + { + StarDifficulty = 3.7, + Ruleset = rulesets.GetRuleset(3), + Metadata = new BeatmapMetadata + { + Title = @"Platina", + Artist = @"Maaya Sakamoto", + Author = @"uwutm8", + }, + BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh/beatmaps/560573/covers/cover.jpg?1492722343", + }, + }, + }, + } + }, + MaxParticipants = { Value = 200 }, + Participants = + { + Value = new[] + { + new User { Username = @"flyte", Id = 3103765, GlobalRank = 1425 }, + new User { Username = @"Cookiezi", Id = 124493, GlobalRank = 5466 }, + new User { Username = @"Angelsim", Id = 1777162, GlobalRank = 2873 }, + new User { Username = @"Rafis", Id = 2558286, GlobalRank = 4687 }, + new User { Username = @"hvick225", Id = 50265, GlobalRank = 3258 }, + new User { Username = @"peppy", Id = 2, GlobalRank = 6251 } + } + } + }; + + RoomInspector inspector; + Add(inspector = new RoomInspector + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Room = room, + }); + + AddStep(@"change title", () => room.Name.Value = @"A Better Room Than The Above"); + AddStep(@"change host", () => room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }); + AddStep(@"change status", () => room.Status.Value = new RoomStatusPlaying()); + AddStep(@"change type", () => room.Type.Value = new GameTypeTag()); + AddStep(@"change beatmap", () => room.Beatmap.Value = null); + AddStep(@"change max participants", () => room.MaxParticipants.Value = null); + AddStep(@"change participants", () => room.Participants.Value = new[] + { + new User { Username = @"filsdelama", Id = 2831793, GlobalRank = 8542 }, + new User { Username = @"_index", Id = 652457, GlobalRank = 15024 } + }); + + AddStep(@"change room", () => + { + var newRoom = new Room + { + Name = { Value = @"My New, Better Than Ever Room" }, + Host = { Value = new User { Username = @"Angelsim", Id = 1777162, Country = new Country { FlagName = @"KR" } } }, + Status = { Value = new RoomStatusOpen() }, + Type = { Value = new GameTypeTagTeam() }, + Beatmap = + { + Value = new BeatmapInfo + { + StarDifficulty = 7.07, + Ruleset = rulesets.GetRuleset(0), + Metadata = new BeatmapMetadata + { + Title = @"FREEDOM DIVE", + Artist = @"xi", + Author = @"Nakagawa-Kanon", + }, + BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh/beatmaps/39804/covers/cover.jpg?1456506845", + }, + }, + }, + }, + }, + MaxParticipants = { Value = 10 }, + Participants = + { + Value = new[] + { + new User { Username = @"Angelsim", Id = 1777162, GlobalRank = 4 }, + new User { Username = @"HappyStick", Id = 256802, GlobalRank = 752 }, + new User { Username = @"-Konpaku-", Id = 2258797, GlobalRank = 571 } + } + } + }; + + inspector.Room = newRoom; + }); + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + this.rulesets = rulesets; + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs b/osu.Desktop.Tests/Visual/TestCaseScoreCounter.cs similarity index 81% rename from osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs rename to osu.Desktop.Tests/Visual/TestCaseScoreCounter.cs index f86fa4dab5..ac96deb209 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseScoreCounter.cs +++ b/osu.Desktop.Tests/Visual/TestCaseScoreCounter.cs @@ -1,24 +1,21 @@ // 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.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.MathUtils; -using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.HUD; +using OpenTK; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseScoreCounter : TestCase + internal class TestCaseScoreCounter : OsuTestCase { public override string Description => @"Tests multiple counters"; - public override void Reset() + public TestCaseScoreCounter() { - base.Reset(); - int numerator = 0, denominator = 0; ScoreCounter score = new ScoreCounter(7) @@ -52,7 +49,7 @@ namespace osu.Desktop.VisualTests.Tests Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -160), - Count = 5, + CountStars = 5, }; Add(stars); @@ -61,7 +58,7 @@ namespace osu.Desktop.VisualTests.Tests Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Position = new Vector2(20, -190), - Text = stars.Count.ToString("0.00"), + Text = stars.CountStars.ToString("0.00"), }; Add(starsLabel); @@ -71,15 +68,16 @@ namespace osu.Desktop.VisualTests.Tests comboCounter.Current.Value = 0; numerator = denominator = 0; accuracyCounter.SetFraction(0, 0); - stars.Count = 0; - starsLabel.Text = stars.Count.ToString("0.00"); + stars.CountStars = 0; + starsLabel.Text = stars.CountStars.ToString("0.00"); }); AddStep(@"Hit! :D", delegate { score.Current.Value += 300 + (ulong)(300.0 * (comboCounter.Current > 0 ? comboCounter.Current - 1 : 0) / 25.0); comboCounter.Increment(); - numerator++; denominator++; + numerator++; + denominator++; accuracyCounter.SetFraction(numerator, denominator); }); @@ -92,8 +90,8 @@ namespace osu.Desktop.VisualTests.Tests AddStep(@"Alter stars", delegate { - stars.Count = RNG.NextSingle() * (stars.StarCount + 1); - starsLabel.Text = stars.Count.ToString("0.00"); + stars.CountStars = RNG.NextSingle() * (stars.StarCount + 1); + starsLabel.Text = stars.CountStars.ToString("0.00"); }); AddStep(@"Stop counters", delegate diff --git a/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs b/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs new file mode 100644 index 0000000000..2d49b2d0f9 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs @@ -0,0 +1,226 @@ +// 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 NUnit.Framework; +using OpenTK; +using osu.Desktop.Tests.Beatmaps; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Timing; +using osu.Game.Rulesets.UI; + +namespace osu.Desktop.Tests.Visual +{ + /// + /// The most minimal implementation of a playfield with scrolling hit objects. + /// + [TestFixture] + public class TestCaseScrollingPlayfield : OsuTestCase + { + public TestCaseScrollingPlayfield() + { + Clock = new FramedClock(); + + var objects = new List(); + + int time = 1500; + for (int i = 0; i < 50; i++) + { + objects.Add(new TestHitObject { StartTime = time }); + + time += 500; + } + + Beatmap b = new Beatmap + { + HitObjects = objects, + BeatmapInfo = new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(), + Metadata = new BeatmapMetadata() + } + }; + + WorkingBeatmap beatmap = new TestWorkingBeatmap(b); + + TestRulesetContainer horizontalRulesetContainer; + Add(horizontalRulesetContainer = new TestRulesetContainer(Axes.X, beatmap, true)); + + TestRulesetContainer verticalRulesetContainer; + Add(verticalRulesetContainer = new TestRulesetContainer(Axes.Y, beatmap, true)); + + AddStep("Reverse direction", () => + { + horizontalRulesetContainer.Playfield.Reversed.Toggle(); + verticalRulesetContainer.Playfield.Reversed.Toggle(); + }); + } + + [Test] + public void TestSpeedAdjustmentOrdering() + { + var hitObjectContainer = new ScrollingPlayfield.ScrollingHitObjectContainer(Axes.X); + + var speedAdjustments = new[] + { + new SpeedAdjustmentContainer(new MultiplierControlPoint()), + new SpeedAdjustmentContainer(new MultiplierControlPoint(1000) + { + TimingPoint = new TimingControlPoint { BeatLength = 500 } + }), + new SpeedAdjustmentContainer(new MultiplierControlPoint(2000) + { + TimingPoint = new TimingControlPoint { BeatLength = 1000 }, + DifficultyPoint = new DifficultyControlPoint { SpeedMultiplier = 2} + }), + new SpeedAdjustmentContainer(new MultiplierControlPoint(3000) + { + TimingPoint = new TimingControlPoint { BeatLength = 1000 }, + DifficultyPoint = new DifficultyControlPoint { SpeedMultiplier = 1} + }), + }; + + var hitObjects = new[] + { + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = -1000 }), + new DrawableTestHitObject(Axes.X, new TestHitObject()), + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 1000 }), + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 2000 }), + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 3000 }), + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 4000 }), + }; + + hitObjects.ForEach(h => hitObjectContainer.Add(h)); + speedAdjustments.ForEach(hitObjectContainer.AddSpeedAdjustment); + + // The 0th index in hitObjectContainer.SpeedAdjustments is the "default" control point + // Check multiplier of the default speed adjustment + Assert.AreEqual(1, hitObjectContainer.SpeedAdjustments[0].ControlPoint.Multiplier); + Assert.AreEqual(1, speedAdjustments[0].ControlPoint.Multiplier); + Assert.AreEqual(2, speedAdjustments[1].ControlPoint.Multiplier); + Assert.AreEqual(2, speedAdjustments[2].ControlPoint.Multiplier); + Assert.AreEqual(1, speedAdjustments[3].ControlPoint.Multiplier); + + // Check insertion of hit objects + Assert.IsTrue(hitObjectContainer.SpeedAdjustments[0].Contains(hitObjects[0])); + Assert.IsTrue(speedAdjustments[0].Contains(hitObjects[1])); + Assert.IsTrue(speedAdjustments[1].Contains(hitObjects[2])); + Assert.IsTrue(speedAdjustments[2].Contains(hitObjects[3])); + Assert.IsTrue(speedAdjustments[3].Contains(hitObjects[4])); + Assert.IsTrue(speedAdjustments[3].Contains(hitObjects[5])); + + hitObjectContainer.RemoveSpeedAdjustment(speedAdjustments[1]); + + // The hit object contained in this speed adjustment should be resorted into the previous one + + Assert.IsTrue(speedAdjustments[0].Contains(hitObjects[2])); + } + + private class TestRulesetContainer : ScrollingRulesetContainer + { + private readonly Axes scrollingAxes; + + public TestRulesetContainer(Axes scrollingAxes, WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(null, beatmap, isForCurrentRuleset) + { + this.scrollingAxes = scrollingAxes; + } + + public new TestPlayfield Playfield => base.Playfield; + + public override ScoreProcessor CreateScoreProcessor() => new TestScoreProcessor(); + + public override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); + + protected override BeatmapConverter CreateBeatmapConverter() => new TestBeatmapConverter(); + + protected override Playfield CreatePlayfield() => new TestPlayfield(scrollingAxes); + + protected override DrawableHitObject GetVisualRepresentation(TestHitObject h) => new DrawableTestHitObject(scrollingAxes, h); + } + + private class TestScoreProcessor : ScoreProcessor + { + protected override void OnNewJudgement(TestJudgement judgement) + { + } + } + + private class TestBeatmapConverter : BeatmapConverter + { + protected override IEnumerable ValidConversionTypes => new[] { typeof(HitObject) }; + + protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) + { + yield return original as TestHitObject; + } + } + + private class DrawableTestHitObject : DrawableScrollingHitObject + { + public DrawableTestHitObject(Axes scrollingAxes, TestHitObject hitObject) + : base(hitObject) + { + Anchor = scrollingAxes == Axes.Y ? Anchor.TopCentre : Anchor.CentreLeft; + Origin = Anchor.Centre; + + AutoSizeAxes = Axes.Both; + + Add(new Circle + { + Size = new Vector2(50) + }); + } + + protected override TestJudgement CreateJudgement() => new TestJudgement(); + + protected override void UpdateState(ArmedState state) + { + } + } + + private class TestPlayfield : ScrollingPlayfield + { + protected override Container Content => content; + private readonly Container content; + + public TestPlayfield(Axes scrollingAxes) + : base(scrollingAxes) + { + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f + }, + content = new Container { RelativeSizeAxes = Axes.Both } + }; + } + } + + + private class TestHitObject : HitObject + { + } + + private class TestJudgement : Judgement + { + public override string ResultString { get { throw new NotImplementedException(); } } + public override string MaxResultString { get { throw new NotImplementedException(); } } + } + } +} diff --git a/osu.Desktop.Tests/Visual/TestCaseSettings.cs b/osu.Desktop.Tests/Visual/TestCaseSettings.cs new file mode 100644 index 0000000000..1f4b88e9e3 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseSettings.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Overlays; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseSettings : OsuTestCase + { + public override string Description => @"Tests the settings overlay"; + + private readonly SettingsOverlay settings; + + public TestCaseSettings() + { + Children = new[] { settings = new MainSettings() }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + settings.ToggleVisibility(); + } + } +} diff --git a/osu.Desktop.Tests/Visual/TestCaseSkipButton.cs b/osu.Desktop.Tests/Visual/TestCaseSkipButton.cs new file mode 100644 index 0000000000..0e73314850 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseSkipButton.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Screens.Play; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseSkipButton : OsuTestCase + { + public override string Description => @"Skip skip skippediskip"; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Add(new SkipButton(Clock.CurrentTime + 5000)); + } + } +} diff --git a/osu.Desktop.Tests/Visual/TestCaseSocial.cs b/osu.Desktop.Tests/Visual/TestCaseSocial.cs new file mode 100644 index 0000000000..da60c82cea --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseSocial.cs @@ -0,0 +1,82 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Overlays; +using osu.Game.Users; + +namespace osu.Desktop.Tests.Visual +{ + public class TestCaseSocial : OsuTestCase + { + public override string Description => @"social browser overlay"; + + public TestCaseSocial() + { + SocialOverlay s = new SocialOverlay + { + Users = new[] + { + new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + }, + new User + { + Username = @"Cookiezi", + Id = 124493, + Country = new Country { FlagName = @"KR" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + }, + new User + { + Username = @"Angelsim", + Id = 1777162, + Country = new Country { FlagName = @"KR" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, + new User + { + Username = @"Rafis", + Id = 2558286, + Country = new Country { FlagName = @"PL" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg", + }, + new User + { + Username = @"hvick225", + Id = 50265, + Country = new Country { FlagName = @"TW" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", + }, + new User + { + Username = @"peppy", + Id = 2, + Country = new Country { FlagName = @"AU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + }, + new User + { + Username = @"filsdelama", + Id = 2831793, + Country = new Country { FlagName = @"FR" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c7.jpg" + }, + new User + { + Username = @"_index", + Id = 652457, + Country = new Country { FlagName = @"RU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c8.jpg" + }, + }, + }; + Add(s); + + AddStep(@"toggle", s.ToggleVisibility); + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs b/osu.Desktop.Tests/Visual/TestCaseSongProgress.cs similarity index 82% rename from osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs rename to osu.Desktop.Tests/Visual/TestCaseSongProgress.cs index e3c343f5f8..fceb773ae6 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseSongProgress.cs +++ b/osu.Desktop.Tests/Visual/TestCaseSongProgress.cs @@ -4,26 +4,23 @@ using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.MathUtils; -using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Play; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseSongProgress : TestCase + internal class TestCaseSongProgress : OsuTestCase { public override string Description => @"With fake data"; - private SongProgress progress; - private SongProgressGraph graph; + private readonly SongProgress progress; + private readonly SongProgressGraph graph; - private StopwatchClock clock; + private readonly StopwatchClock clock; - public override void Reset() + public TestCaseSongProgress() { - base.Reset(); - clock = new StopwatchClock(true); Add(progress = new SongProgress diff --git a/osu.Desktop.VisualTests/Tests/TestCaseTabControl.cs b/osu.Desktop.Tests/Visual/TestCaseTabControl.cs similarity index 83% rename from osu.Desktop.VisualTests/Tests/TestCaseTabControl.cs rename to osu.Desktop.Tests/Visual/TestCaseTabControl.cs index 96933a15e7..4bdea2615d 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseTabControl.cs +++ b/osu.Desktop.Tests/Visual/TestCaseTabControl.cs @@ -2,22 +2,19 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using OpenTK; -using osu.Framework.Testing; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Select.Filter; +using OpenTK; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - public class TestCaseTabControl : TestCase + public class TestCaseTabControl : OsuTestCase { public override string Description => @"Filter for song select"; - public override void Reset() + public TestCaseTabControl() { - base.Reset(); - OsuSpriteText text; OsuTabControl filter; Add(filter = new OsuTabControl diff --git a/osu.Desktop.VisualTests/Tests/TestCaseTaikoPlayfield.cs b/osu.Desktop.Tests/Visual/TestCaseTaikoPlayfield.cs similarity index 58% rename from osu.Desktop.VisualTests/Tests/TestCaseTaikoPlayfield.cs rename to osu.Desktop.Tests/Visual/TestCaseTaikoPlayfield.cs index 259d0267db..681805ced0 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseTaikoPlayfield.cs +++ b/osu.Desktop.Tests/Visual/TestCaseTaikoPlayfield.cs @@ -1,24 +1,30 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; +using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.MathUtils; -using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.UI; -using System; +using OpenTK; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps; +using osu.Desktop.Tests.Beatmaps; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseTaikoPlayfield : TestCase + internal class TestCaseTaikoPlayfield : OsuTestCase { - private const double default_duration = 300; + private const double default_duration = 1000; private const float scroll_time = 1000; public override string Description => "Taiko playfield"; @@ -26,14 +32,14 @@ namespace osu.Desktop.VisualTests.Tests protected override double TimePerAction => default_duration * 2; private readonly Random rng = new Random(1337); - private TaikoPlayfield playfield; + private TaikoRulesetContainer rulesetContainer; private Container playfieldContainer; - public override void Reset() + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) { - base.Reset(); - - AddStep("Hit!", addHitJudgement); + AddStep("Hit!", () => addHitJudgement(false)); + AddStep("Kiai hit", () => addHitJudgement(true)); AddStep("Miss :(", addMissJudgement); AddStep("DrumRoll", () => addDrumRoll(false)); AddStep("Strong DrumRoll", () => addDrumRoll(true)); @@ -51,6 +57,25 @@ namespace osu.Desktop.VisualTests.Tests AddStep("Height test 5", () => changePlayfieldSize(5)); AddStep("Reset height", () => changePlayfieldSize(6)); + var controlPointInfo = new ControlPointInfo(); + controlPointInfo.TimingPoints.Add(new TimingControlPoint()); + + WorkingBeatmap beatmap = new TestWorkingBeatmap(new Beatmap + { + HitObjects = new List { new CentreHit() }, + BeatmapInfo = new BeatmapInfo + { + Difficulty = new BeatmapDifficulty(), + Metadata = new BeatmapMetadata + { + Artist = @"Unknown", + Title = @"Sample Beatmap", + Author = @"peppy", + }, + }, + ControlPointInfo = controlPointInfo + }); + var rateAdjustClock = new StopwatchClock(true) { Rate = 1 }; Add(playfieldContainer = new Container @@ -58,17 +83,16 @@ namespace osu.Desktop.VisualTests.Tests Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, - Height = TaikoPlayfield.DEFAULT_PLAYFIELD_HEIGHT, + Height = 768, Clock = new FramedClock(rateAdjustClock), - Children = new[] - { - playfield = new TaikoPlayfield() - } + Children = new[] { rulesetContainer = new TaikoRulesetContainer(rulesets.GetRuleset(1).CreateInstance(), beatmap, true) } }); } private void changePlayfieldSize(int step) { + double delay = 0; + // Add new hits switch (step) { @@ -85,8 +109,8 @@ namespace osu.Desktop.VisualTests.Tests addDrumRoll(true); break; case 5: - addSwell(1000); - playfieldContainer.Delay(scroll_time - 100); + addSwell(); + delay = scroll_time - 100; break; } @@ -94,19 +118,28 @@ namespace osu.Desktop.VisualTests.Tests switch (step) { default: - playfieldContainer.ResizeTo(new Vector2(1, rng.Next(25, 400)), 500); + playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500); break; case 6: - playfieldContainer.ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_PLAYFIELD_HEIGHT), 500); + playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); break; } } - private void addHitJudgement() + private void addHitJudgement(bool kiai) { TaikoHitResult hitResult = RNG.Next(2) == 0 ? TaikoHitResult.Good : TaikoHitResult.Great; - var h = new DrawableTestHit(new Hit()) + var cpi = new ControlPointInfo(); + cpi.EffectPoints.Add(new EffectControlPoint + { + KiaiMode = kiai + }); + + Hit hit = new Hit(); + hit.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == TaikoHitResult.Good ? -0.1f : -0.05f, hitResult == TaikoHitResult.Good ? 0.1f : 0.05f), Judgement = new TaikoJudgement @@ -117,18 +150,18 @@ namespace osu.Desktop.VisualTests.Tests } }; - playfield.OnJudgement(h); + rulesetContainer.Playfield.OnJudgement(h); if (RNG.Next(10) == 0) { h.Judgement.SecondHit = true; - playfield.OnJudgement(h); + rulesetContainer.Playfield.OnJudgement(h); } } private void addMissJudgement() { - playfield.OnJudgement(new DrawableTestHit(new Hit()) + rulesetContainer.Playfield.OnJudgement(new DrawableTestHit(new Hit()) { Judgement = new TaikoJudgement { @@ -140,22 +173,17 @@ namespace osu.Desktop.VisualTests.Tests private void addBarLine(bool major, double delay = scroll_time) { - BarLine bl = new BarLine - { - StartTime = playfield.Time.Current + delay, - ScrollTime = scroll_time - }; + BarLine bl = new BarLine { StartTime = rulesetContainer.Playfield.Time.Current + delay }; - playfield.AddBarLine(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); + rulesetContainer.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); } private void addSwell(double duration = default_duration) { - playfield.Add(new DrawableSwell(new Swell + rulesetContainer.Playfield.Add(new DrawableSwell(new Swell { - StartTime = playfield.Time.Current + scroll_time, + StartTime = rulesetContainer.Playfield.Time.Current + scroll_time, Duration = duration, - ScrollTime = scroll_time })); } @@ -166,41 +194,40 @@ namespace osu.Desktop.VisualTests.Tests var d = new DrumRoll { - StartTime = playfield.Time.Current + scroll_time, + StartTime = rulesetContainer.Playfield.Time.Current + scroll_time, IsStrong = strong, Duration = duration, - ScrollTime = scroll_time, }; - playfield.Add(new DrawableDrumRoll(d)); + rulesetContainer.Playfield.Add(new DrawableDrumRoll(d)); } private void addCentreHit(bool strong) { Hit h = new Hit { - StartTime = playfield.Time.Current + scroll_time, - ScrollTime = scroll_time + StartTime = rulesetContainer.Playfield.Time.Current + scroll_time, + IsStrong = strong }; if (strong) - playfield.Add(new DrawableCentreHitStrong(h)); + rulesetContainer.Playfield.Add(new DrawableCentreHitStrong(h)); else - playfield.Add(new DrawableCentreHit(h)); + rulesetContainer.Playfield.Add(new DrawableCentreHit(h)); } private void addRimHit(bool strong) { Hit h = new Hit { - StartTime = playfield.Time.Current + scroll_time, - ScrollTime = scroll_time + StartTime = rulesetContainer.Playfield.Time.Current + scroll_time, + IsStrong = strong }; if (strong) - playfield.Add(new DrawableRimHitStrong(h)); + rulesetContainer.Playfield.Add(new DrawableRimHitStrong(h)); else - playfield.Add(new DrawableRimHit(h)); + rulesetContainer.Playfield.Add(new DrawableRimHit(h)); } private class DrawableTestHit : DrawableHitObject diff --git a/osu.Desktop.VisualTests/Tests/TestCaseTextAwesome.cs b/osu.Desktop.Tests/Visual/TestCaseTextAwesome.cs similarity index 79% rename from osu.Desktop.VisualTests/Tests/TestCaseTextAwesome.cs rename to osu.Desktop.Tests/Visual/TestCaseTextAwesome.cs index 7182ee7c06..b98c0f700d 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseTextAwesome.cs +++ b/osu.Desktop.Tests/Visual/TestCaseTextAwesome.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Testing; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.MathUtils; @@ -10,16 +9,14 @@ using osu.Game.Graphics; using OpenTK; using OpenTK.Graphics; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseTextAwesome : TestCase + internal class TestCaseTextAwesome : OsuTestCase { public override string Description => @"Tests display of icons"; - public override void Reset() + public TestCaseTextAwesome() { - base.Reset(); - FillFlowContainer flow; Add(flow = new FillFlowContainer @@ -33,10 +30,10 @@ namespace osu.Desktop.VisualTests.Tests int i = 50; foreach (FontAwesome fa in Enum.GetValues(typeof(FontAwesome))) { - flow.Add(new TextAwesome + flow.Add(new SpriteIcon { Icon = fa, - TextSize = 60, + Size = new Vector2(60), Colour = new Color4( Math.Max(0.5f, RNG.NextSingle()), Math.Max(0.5f, RNG.NextSingle()), diff --git a/osu.Desktop.Tests/Visual/TestCaseTwoLayerButton.cs b/osu.Desktop.Tests/Visual/TestCaseTwoLayerButton.cs new file mode 100644 index 0000000000..ac641d75a2 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseTwoLayerButton.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Graphics.UserInterface; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseTwoLayerButton : OsuTestCase + { + public override string Description => @"Mostly back button"; + + public TestCaseTwoLayerButton() + { + Add(new BackButton()); + } + } +} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseUserPanel.cs b/osu.Desktop.Tests/Visual/TestCaseUserPanel.cs similarity index 89% rename from osu.Desktop.VisualTests/Tests/TestCaseUserPanel.cs rename to osu.Desktop.Tests/Visual/TestCaseUserPanel.cs index 92d58c10c9..b42fd3136d 100644 --- a/osu.Desktop.VisualTests/Tests/TestCaseUserPanel.cs +++ b/osu.Desktop.Tests/Visual/TestCaseUserPanel.cs @@ -1,22 +1,19 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Testing; using osu.Framework.Graphics; -using osu.Game.Users; using osu.Framework.Graphics.Containers; +using osu.Game.Users; using OpenTK; -namespace osu.Desktop.VisualTests.Tests +namespace osu.Desktop.Tests.Visual { - internal class TestCaseUserPanel : TestCase + internal class TestCaseUserPanel : OsuTestCase { public override string Description => @"Panels for displaying a user's status"; - public override void Reset() + public TestCaseUserPanel() { - base.Reset(); - UserPanel flyte; UserPanel peppy; Add(new FillFlowContainer diff --git a/osu.Desktop.Tests/Visual/TestCaseUserProfile.cs b/osu.Desktop.Tests/Visual/TestCaseUserProfile.cs new file mode 100644 index 0000000000..e5955441dc --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseUserProfile.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Game.Overlays; +using osu.Game.Users; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseUserProfile : OsuTestCase + { + public override string Description => "Tests user's profile page."; + + public TestCaseUserProfile() + { + var profile = new UserProfileOverlay(); + Add(profile); + + AddStep("Show offline dummy", () => profile.ShowUser(new User + { + Username = @"Somebody", + Id = 1, + Country = new Country { FullName = @"Alien" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", + JoinDate = DateTimeOffset.Now.AddDays(-1), + LastVisit = DateTimeOffset.Now, + Age = 1, + ProfileOrder = new[] { "me" }, + CountryRank = 1, + Statistics = new UserStatistics + { + Rank = 2148, + PP = 4567.89m + }, + AllRankHistories = new User.RankHistories + { + Osu = new User.RankHistory + { + Mode = @"osu", + Data = Enumerable.Range(2345,45).Concat(Enumerable.Range(2109,40)).ToArray() + } + } + }, false)); + AddStep("Show ppy", () => profile.ShowUser(new User + { + Username = @"peppy", + Id = 2, + Country = new Country { FullName = @"Australia", FlagName = @"AU" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg" + })); + AddStep("Show flyte", () => profile.ShowUser(new User + { + Username = @"flyte", + Id = 3103765, + Country = new Country { FullName = @"Japan", FlagName = @"JP" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + })); + AddStep("Hide", profile.Hide); + AddStep("Show without reload", profile.Show); + } + } +} diff --git a/osu.Desktop.Tests/VisualTests.cs b/osu.Desktop.Tests/VisualTests.cs deleted file mode 100644 index 6ef924e873..0000000000 --- a/osu.Desktop.Tests/VisualTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; -using osu.Desktop.VisualTests; -using osu.Framework.Desktop.Platform; - -namespace osu.Desktop.Tests -{ - [TestFixture] - public class VisualTests - { - [Test] - public void TestVisualTests() - { - using (var host = new HeadlessGameHost()) - { - host.Run(new AutomatedVisualTestGame()); - } - } - } -} diff --git a/osu.Desktop.Tests/app.config b/osu.Desktop.Tests/app.config new file mode 100644 index 0000000000..faeaf001de --- /dev/null +++ b/osu.Desktop.Tests/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/osu.Desktop.Tests/osu.Desktop.Tests.csproj b/osu.Desktop.Tests/osu.Desktop.Tests.csproj index f0620c98ef..24d112a45c 100644 --- a/osu.Desktop.Tests/osu.Desktop.Tests.csproj +++ b/osu.Desktop.Tests/osu.Desktop.Tests.csproj @@ -37,8 +37,13 @@ $(SolutionDir)\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll - - $(SolutionDir)\packages\NUnit.3.6.1\lib\net45\nunit.framework.dll + + $(SolutionDir)\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll + True + + + $(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll + True False @@ -54,15 +59,68 @@ $(SolutionDir)\packages\SQLite.Net-PCL.3.1.1\lib\net40\SQLite.Net.Platform.Generic.dll + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {65DC628F-A640-4111-AB35-3A5652BC1E17} osu.Framework.Desktop + + {007b2356-ab6f-4bd9-96d5-116fc2dce69a} + osu.Framework.Testing + {C76BF5B3-985E-4D39-95FE-97C9C879B83A} osu.Framework @@ -71,10 +129,6 @@ {d9a367c9-4c1a-489f-9b05-a0cea2b53b58} osu.Game.Resources - - {69051C69-12AE-4E7D-A3E6-460D2E282312} - osu.Desktop.VisualTests - {58F6C80C-1253-4A0E-A465-B8C85EBEADF3} osu.Game.Rulesets.Catch @@ -100,6 +154,7 @@ osu.licenseheader + diff --git a/osu.Desktop.Tests/packages.config b/osu.Desktop.Tests/packages.config index ad51a60195..ed487e5cd5 100644 --- a/osu.Desktop.Tests/packages.config +++ b/osu.Desktop.Tests/packages.config @@ -5,7 +5,8 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste --> - + + diff --git a/osu.Desktop.VisualTests/AutomatedVisualTestGame.cs b/osu.Desktop.VisualTests/AutomatedVisualTestGame.cs deleted file mode 100644 index b08588b29c..0000000000 --- a/osu.Desktop.VisualTests/AutomatedVisualTestGame.cs +++ /dev/null @@ -1,20 +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.Testing; -using osu.Game; - -namespace osu.Desktop.VisualTests -{ - public class AutomatedVisualTestGame : OsuGameBase - { - protected override void LoadComplete() - { - base.LoadComplete(); - - // Have to construct this here, rather than in the constructor, because - // we depend on some dependencies to be loaded within OsuGameBase.load(). - Add(new TestRunner(new TestBrowser())); - } - } -} \ No newline at end of file diff --git a/osu.Desktop.VisualTests/Program.cs b/osu.Desktop.VisualTests/Program.cs index 03d1588b78..62465c69d2 100644 --- a/osu.Desktop.VisualTests/Program.cs +++ b/osu.Desktop.VisualTests/Program.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Desktop; using osu.Framework.Platform; +using osu.Framework.VisualTests; namespace osu.Desktop.VisualTests { diff --git a/osu.Desktop.VisualTests/Tests/TestCaseDrawableRoom.cs b/osu.Desktop.VisualTests/Tests/TestCaseDrawableRoom.cs deleted file mode 100644 index de58323abe..0000000000 --- a/osu.Desktop.VisualTests/Tests/TestCaseDrawableRoom.cs +++ /dev/null @@ -1,74 +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.Graphics.Containers; -using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Screens.Multiplayer; -using osu.Game.Online.Multiplayer; -using osu.Game.Users; -using osu.Game.Database; - -namespace osu.Desktop.VisualTests.Tests -{ - internal class TestCaseDrawableRoom : TestCase - { - public override string Description => @"Select your favourite room"; - - public override void Reset() - { - base.Reset(); - - DrawableRoom first; - DrawableRoom second; - Add(new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - Width = 500f, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - first = new DrawableRoom(new Room()), - second = new DrawableRoom(new Room()), - } - }); - - first.Room.Name.Value = @"Great Room Right Here"; - first.Room.Host.Value = new User { Username = @"Naeferith", Id = 9492835, Country = new Country { FlagName = @"FR" }}; - first.Room.Status.Value = new RoomStatusOpen(); - first.Room.Beatmap.Value = new BeatmapMetadata { Title = @"Seiryu", Artist = @"Critical Crystal" }; - - second.Room.Name.Value = @"Relax It's The Weekend"; - second.Room.Host.Value = new User { Username = @"peppy", Id = 2, Country = new Country { FlagName = @"AU" }}; - second.Room.Status.Value = new RoomStatusPlaying(); - second.Room.Beatmap.Value = new BeatmapMetadata { Title = @"ZAQ", Artist = @"Serendipity" }; - - AddStep(@"change state", () => - { - first.Room.Status.Value = new RoomStatusPlaying(); - }); - - AddStep(@"change name", () => - { - first.Room.Name.Value = @"I Changed Name"; - }); - - AddStep(@"change host", () => - { - first.Room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }; - }); - - AddStep(@"change beatmap", () => - { - first.Room.Beatmap.Value = null; - }); - - AddStep(@"change state", () => - { - first.Room.Status.Value = new RoomStatusOpen(); - }); - } - } -} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseKeyCounter.cs b/osu.Desktop.VisualTests/Tests/TestCaseKeyCounter.cs deleted file mode 100644 index b1b9ddbcda..0000000000 --- a/osu.Desktop.VisualTests/Tests/TestCaseKeyCounter.cs +++ /dev/null @@ -1,104 +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.Testing; -using osu.Framework.Graphics; -using OpenTK.Input; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Configuration; -using osu.Framework.Graphics.Containers; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.MathUtils; -using osu.Framework.Graphics.Sprites; -using osu.Game.Screens.Play; - -namespace osu.Desktop.VisualTests.Tests -{ - internal class TestCaseKeyCounter : TestCase - { - public override string Description => @"Tests key counter"; - - public override void Reset() - { - base.Reset(); - - KeyCounterCollection kc = new KeyCounterCollection - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - IsCounting = true, - Children = new KeyCounter[] - { - new KeyCounterKeyboard(Key.Z), - new KeyCounterKeyboard(Key.X), - new KeyCounterMouse(MouseButton.Left), - new KeyCounterMouse(MouseButton.Right), - }, - }; - BindableInt bindable = new BindableInt { MinValue = 0, MaxValue = 200, Default = 50 }; - bindable.ValueChanged += delegate { kc.FadeTime = bindable.Value; }; - AddStep("Add Random", () => - { - Key key = (Key)((int)Key.A + RNG.Next(26)); - kc.Add(new KeyCounterKeyboard(key)); - }); - - TestSliderBar sliderBar; - - Add(new Container - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new SpriteText { Text = "FadeTime" }, - sliderBar =new TestSliderBar - { - Width = 150, - Height = 10, - SelectionColor = Color4.Orange, - } - } - }); - - sliderBar.Current.BindTo(bindable); - - Add(kc); - } - private class TestSliderBar : SliderBar where T : struct - { - public Color4 Color - { - get { return Box.Colour; } - set { Box.Colour = value; } - } - - public Color4 SelectionColor - { - get { return SelectionBox.Colour; } - set { SelectionBox.Colour = value; } - } - - protected readonly Box SelectionBox; - protected readonly Box Box; - - public TestSliderBar() - { - Children = new Drawable[] - { - Box = new Box { RelativeSizeAxes = Axes.Both }, - SelectionBox = new Box { RelativeSizeAxes = Axes.Both } - }; - } - - protected override void UpdateValue(float value) - { - SelectionBox.ScaleTo( - new Vector2(value, 1), - 300, EasingTypes.OutQuint); - } - } - } -} diff --git a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs deleted file mode 100644 index f28cdd6a7e..0000000000 --- a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using OpenTK; -using osu.Framework.Graphics.Sprites; -using osu.Game.Database; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Play; -using OpenTK.Graphics; -using osu.Desktop.VisualTests.Beatmaps; -using osu.Game.Rulesets.Osu.UI; - -namespace osu.Desktop.VisualTests.Tests -{ - internal class TestCasePlayer : TestCase - { - protected Player Player; - private BeatmapDatabase db; - private RulesetDatabase rulesets; - - public override string Description => @"Showing everything to play the game."; - - [BackgroundDependencyLoader] - private void load(BeatmapDatabase db, RulesetDatabase rulesets) - { - this.rulesets = rulesets; - this.db = db; - } - - public override void Reset() - { - base.Reset(); - - WorkingBeatmap beatmap = null; - - var beatmapInfo = db.Query().FirstOrDefault(b => b.RulesetID == 0); - if (beatmapInfo != null) - beatmap = db.GetWorkingBeatmap(beatmapInfo); - - if (beatmap?.Track == null) - { - var objects = new List(); - - int time = 1500; - for (int i = 0; i < 50; i++) - { - objects.Add(new HitCircle - { - StartTime = time, - Position = new Vector2(i % 4 == 0 || i % 4 == 2 ? 0 : OsuPlayfield.BASE_SIZE.X, - i % 4 < 2 ? 0 : OsuPlayfield.BASE_SIZE.Y), - NewCombo = i % 4 == 0 - }); - - time += 500; - } - - Beatmap b = new Beatmap - { - HitObjects = objects, - BeatmapInfo = new BeatmapInfo - { - Difficulty = new BeatmapDifficulty(), - Ruleset = rulesets.Query().First(), - Metadata = new BeatmapMetadata - { - Artist = @"Unknown", - Title = @"Sample Beatmap", - Author = @"peppy", - } - } - }; - - beatmap = new TestWorkingBeatmap(b); - } - - Add(new Box - { - RelativeSizeAxes = Framework.Graphics.Axes.Both, - Colour = Color4.Black, - }); - - Add(Player = CreatePlayer(beatmap)); - } - - protected virtual Player CreatePlayer(WorkingBeatmap beatmap) - { - return new Player - { - Beatmap = beatmap - }; - } - } -} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseSettings.cs b/osu.Desktop.VisualTests/Tests/TestCaseSettings.cs deleted file mode 100644 index 660085e558..0000000000 --- a/osu.Desktop.VisualTests/Tests/TestCaseSettings.cs +++ /dev/null @@ -1,23 +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.Testing; -using osu.Game.Overlays; - -namespace osu.Desktop.VisualTests.Tests -{ - internal class TestCaseSettings : TestCase - { - public override string Description => @"Tests the settings overlay"; - - private SettingsOverlay settings; - - public override void Reset() - { - base.Reset(); - - Children = new[] { settings = new SettingsOverlay() }; - settings.ToggleVisibility(); - } - } -} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseTaikoHitObjects.cs b/osu.Desktop.VisualTests/Tests/TestCaseTaikoHitObjects.cs deleted file mode 100644 index d769071bd9..0000000000 --- a/osu.Desktop.VisualTests/Tests/TestCaseTaikoHitObjects.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Linq; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; - -namespace osu.Desktop.VisualTests.Tests -{ - internal class TestCaseTaikoHitObjects : TestCase - { - public override string Description => "Taiko hit objects"; - - private bool kiai; - - public override void Reset() - { - base.Reset(); - - AddToggleStep("Kiai", b => - { - kiai = !kiai; - updateKiaiState(); - }); - - Add(new CirclePiece - { - Position = new Vector2(100, 100), - AccentColour = Color4.DarkRed, - KiaiMode = kiai, - Children = new[] - { - new CentreHitSymbolPiece() - } - }); - - Add(new CirclePiece(true) - { - Position = new Vector2(350, 100), - AccentColour = Color4.DarkRed, - KiaiMode = kiai, - Children = new[] - { - new CentreHitSymbolPiece() - } - }); - - Add(new CirclePiece - { - Position = new Vector2(100, 300), - AccentColour = Color4.DarkBlue, - KiaiMode = kiai, - Children = new[] - { - new RimHitSymbolPiece() - } - }); - - Add(new CirclePiece(true) - { - Position = new Vector2(350, 300), - AccentColour = Color4.DarkBlue, - KiaiMode = kiai, - Children = new[] - { - new RimHitSymbolPiece() - } - }); - - Add(new CirclePiece - { - Position = new Vector2(100, 500), - AccentColour = Color4.Orange, - KiaiMode = kiai, - Children = new[] - { - new SwellSymbolPiece() - } - }); - - Add(new ElongatedCirclePiece - { - Position = new Vector2(575, 100), - AccentColour = Color4.Orange, - KiaiMode = kiai, - Length = 0.10f, - PlayfieldLengthReference = () => DrawSize.X - }); - - Add(new ElongatedCirclePiece(true) - { - Position = new Vector2(575, 300), - AccentColour = Color4.Orange, - KiaiMode = kiai, - Length = 0.10f, - PlayfieldLengthReference = () => DrawSize.X - }); - } - - private void updateKiaiState() - { - foreach (var c in Children.OfType()) - c.KiaiMode = kiai; - } - - private abstract class BaseCircle : Container - { - protected readonly CirclePiece Piece; - - protected BaseCircle(CirclePiece piece) - { - Piece = piece; - - Add(Piece); - } - } - } -} diff --git a/osu.Desktop.VisualTests/Tests/TestCaseTwoLayerButton.cs b/osu.Desktop.VisualTests/Tests/TestCaseTwoLayerButton.cs deleted file mode 100644 index ba17cfc3d8..0000000000 --- a/osu.Desktop.VisualTests/Tests/TestCaseTwoLayerButton.cs +++ /dev/null @@ -1,22 +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.Testing; -using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Play; - -namespace osu.Desktop.VisualTests.Tests -{ - internal class TestCaseTwoLayerButton : TestCase - { - public override string Description => @"Back and skip and what not"; - - public override void Reset() - { - base.Reset(); - - Add(new BackButton()); - Add(new SkipButton(Clock.CurrentTime + 5000)); - } - } -} diff --git a/osu.Desktop.VisualTests/VisualTestGame.cs b/osu.Desktop.VisualTests/VisualTestGame.cs index 5c5bcd9e21..7655f6a59d 100644 --- a/osu.Desktop.VisualTests/VisualTestGame.cs +++ b/osu.Desktop.VisualTests/VisualTestGame.cs @@ -2,7 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Platform; -using osu.Framework.Testing; +using osu.Framework.VisualTests; using osu.Game; using osu.Game.Screens.Backgrounds; diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index 850c38cc24..8bba59207f 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -1,4 +1,4 @@ - + {69051C69-12AE-4E7D-A3E6-460D2E282312} @@ -87,11 +87,13 @@ $(SolutionDir)\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll - - $(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll + + $(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll + True - - $(SolutionDir)\packages\SharpCompress.0.15.2\lib\net45\SharpCompress.dll + + $(SolutionDir)\packages\SharpCompress.0.17.1\lib\net45\SharpCompress.dll + True False @@ -183,46 +185,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -247,4 +211,4 @@ - \ No newline at end of file + diff --git a/osu.Desktop.VisualTests/packages.config b/osu.Desktop.VisualTests/packages.config index cad2ffff0d..2fb1023253 100644 --- a/osu.Desktop.VisualTests/packages.config +++ b/osu.Desktop.VisualTests/packages.config @@ -5,8 +5,8 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste --> - - + + diff --git a/osu.Desktop/Beatmaps/IO/LegacyFilesystemReader.cs b/osu.Desktop/Beatmaps/IO/LegacyFilesystemReader.cs deleted file mode 100644 index 8772fc9f28..0000000000 --- a/osu.Desktop/Beatmaps/IO/LegacyFilesystemReader.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.IO; -using System.Linq; -using osu.Game.Beatmaps.IO; - -namespace osu.Desktop.Beatmaps.IO -{ - /// - /// Reads an extracted legacy beatmap from disk. - /// - public class LegacyFilesystemReader : ArchiveReader - { - public static void Register() => AddReader((storage, path) => Directory.Exists(path)); - - private readonly string basePath; - - public LegacyFilesystemReader(string path) - { - basePath = path; - - BeatmapFilenames = Directory.GetFiles(basePath, @"*.osu").Select(Path.GetFileName).ToArray(); - - if (BeatmapFilenames.Length == 0) - throw new FileNotFoundException(@"This directory contains no beatmaps"); - - StoryboardFilename = Directory.GetFiles(basePath, @"*.osb").Select(Path.GetFileName).FirstOrDefault(); - } - - public override Stream GetStream(string name) - { - return File.OpenRead(Path.Combine(basePath, name)); - } - - public override void Dispose() - { - // no-op - } - - public override Stream GetUnderlyingStream() => null; - } -} diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 299f64d998..88c8a206c8 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using osu.Game; using System.Linq; using System.Windows.Forms; @@ -11,6 +12,8 @@ using System.Reflection; using System.Drawing; using System.IO; using System.Threading.Tasks; +using Microsoft.Win32; +using osu.Framework.Graphics.Containers; using osu.Game.Screens.Menu; namespace osu.Desktop @@ -22,18 +25,74 @@ namespace osu.Desktop public OsuGameDesktop(string[] args = null) : base(args) { - versionManager = new VersionManager { Depth = int.MinValue }; + versionManager = new VersionManager + { + Depth = int.MinValue, + State = Visibility.Hidden + }; + } + + public override Storage GetStorageForStableInstall() + { + try + { + return new StableStorage(); + } + catch + { + return null; + } + } + + /// + /// A method of accessing an osu-stable install in a controlled fashion. + /// + private class StableStorage : DesktopStorage + { + protected override string LocateBasePath() + { + Func checkExists = p => Directory.Exists(Path.Combine(p, "Songs")); + + string stableInstallPath; + + try + { + using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(String.Empty).ToString().Split('"')[1].Replace("osu!.exe", ""); + + if (checkExists(stableInstallPath)) + return stableInstallPath; + } + catch + { + } + + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".osu"); + if (checkExists(stableInstallPath)) + return stableInstallPath; + + return null; + } + + public StableStorage() + : base(string.Empty) + { + } } protected override void LoadComplete() { base.LoadComplete(); - LoadComponentAsync(versionManager); + LoadComponentAsync(versionManager, Add); ScreenChanged += s => { - if (!versionManager.IsAlive && s is Intro) - Add(versionManager); + if (!versionManager.IsPresent && s is Intro) + versionManager.State = Visibility.Visible; }; } @@ -45,7 +104,7 @@ namespace osu.Desktop { desktopWindow.CursorState |= CursorState.Hidden; - desktopWindow.Icon = Icon.ExtractAssociatedIcon(Assembly.GetExecutingAssembly().Location); + desktopWindow.Icon = new Icon(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico")); desktopWindow.Title = Name; desktopWindow.DragEnter += dragEnter; @@ -60,11 +119,11 @@ namespace osu.Desktop var filePaths = dropData.Select(f => f.ToString()).ToArray(); if (filePaths.All(f => Path.GetExtension(f) == @".osz")) - Task.Run(() => BeatmapDatabase.Import(filePaths)); + Task.Run(() => BeatmapManager.Import(filePaths)); else if (filePaths.All(f => Path.GetExtension(f) == @".osr")) Task.Run(() => { - var score = ScoreDatabase.ReadReplayFile(filePaths.First()); + var score = ScoreStore.ReadReplayFile(filePaths.First()); Schedule(() => LoadScore(score)); }); } diff --git a/osu.Desktop/OsuTestBrowser.cs b/osu.Desktop/OsuTestBrowser.cs new file mode 100644 index 0000000000..50af9bd317 --- /dev/null +++ b/osu.Desktop/OsuTestBrowser.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game; +using osu.Game.Screens.Backgrounds; + +namespace osu.Desktop +{ + internal class OsuTestBrowser : OsuGameBase + { + protected override void LoadComplete() + { + base.LoadComplete(); + + LoadComponentAsync(new BackgroundScreenDefault { Depth = 10 }, AddInternal); + + // Have to construct this here, rather than in the constructor, because + // we depend on some dependencies to be loaded within OsuGameBase.load(). + Add(new TestBrowser()); + } + + public override void SetHost(GameHost host) + { + base.SetHost(host); + + host.UpdateThread.InactiveHz = host.UpdateThread.ActiveHz; + host.DrawThread.InactiveHz = host.DrawThread.ActiveHz; + host.InputThread.InactiveHz = host.InputThread.ActiveHz; + + host.Window.CursorState |= CursorState.Hidden; + } + } +} \ No newline at end of file diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs index 9532652bfe..b445340f50 100644 --- a/osu.Desktop/Overlays/VersionManager.cs +++ b/osu.Desktop/Overlays/VersionManager.cs @@ -10,6 +10,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using Squirrel; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Graphics; @@ -24,16 +25,14 @@ namespace osu.Desktop.Overlays public class VersionManager : OverlayContainer { private UpdateManager updateManager; - private NotificationManager notificationManager; - - protected override bool HideOnEscape => false; + private NotificationOverlay notificationOverlay; public override bool HandleInput => false; [BackgroundDependencyLoader] - private void load(NotificationManager notification, OsuColour colours, TextureStore textures, OsuGameBase game) + private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game) { - notificationManager = notification; + notificationOverlay = notification; AutoSizeAxes = Axes.Both; Anchor = Anchor.BottomCentre; @@ -92,12 +91,6 @@ namespace osu.Desktop.Overlays checkForUpdateAsync(); } - protected override void LoadComplete() - { - base.LoadComplete(); - State = Visibility.Visible; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -121,7 +114,7 @@ namespace osu.Desktop.Overlays if (notification == null) { notification = new UpdateProgressNotification { State = ProgressNotificationState.Active }; - Schedule(() => notificationManager.Post(notification)); + Schedule(() => notificationOverlay.Post(notification)); } Schedule(() => @@ -180,7 +173,7 @@ namespace osu.Desktop.Overlays protected override void PopIn() { - FadeIn(1000); + this.FadeIn(1000); } protected override void PopOut() @@ -191,7 +184,7 @@ namespace osu.Desktop.Overlays { private OsuGame game; - protected override Notification CreateCompletionNotification() => new ProgressCompletionNotification() + protected override Notification CreateCompletionNotification() => new ProgressCompletionNotification { Text = @"Update ready to install. Click to restart!", Activated = () => @@ -207,20 +200,20 @@ namespace osu.Desktop.Overlays { this.game = game; - IconContent.Add(new Drawable[] + IconContent.AddRange(new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - ColourInfo = ColourInfo.GradientVertical(colours.YellowDark, colours.Yellow) + Colour = ColourInfo.GradientVertical(colours.YellowDark, colours.Yellow) }, - new TextAwesome + new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, Icon = FontAwesome.fa_upload, Colour = Color4.White, - TextSize = 20 + Size = new Vector2(20), } }); } diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 210f780078..1fab92e020 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -3,7 +3,7 @@ using System; using System.IO; -using osu.Desktop.Beatmaps.IO; +using System.Linq; using osu.Framework.Desktop; using osu.Framework.Desktop.Platform; using osu.Game.IPC; @@ -15,8 +15,6 @@ namespace osu.Desktop [STAThread] public static int Main(string[] args) { - LegacyFilesystemReader.Register(); - // Back up the cwd before DesktopGameHost changes it var cwd = Environment.CurrentDirectory; @@ -36,7 +34,16 @@ namespace osu.Desktop } else { - host.Run(new OsuGameDesktop(args)); + switch (args.FirstOrDefault() ?? string.Empty) + { + case "--tests": + host.Run(new OsuTestBrowser()); + break; + default: + host.Run(new OsuGameDesktop(args)); + break; + } + } return 0; } diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 4f66dfd3eb..bbca4145c6 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -90,6 +90,21 @@ Properties\app.manifest + + true + bin\Debug\ + DEBUG + true + 0 + true + full + AnyCPU + false + 6 + prompt + AllRules.ruleset + --tests + $(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.dll @@ -121,18 +136,23 @@ - $(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\NuGet.Squirrel.dll + $(SolutionDir)\packages\squirrel.windows.1.7.5\lib\Net45\NuGet.Squirrel.dll True - - $(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll + + $(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll + True + + + $(SolutionDir)\packages\SharpCompress.0.17.1\lib\net45\SharpCompress.dll + True $(SolutionDir)\packages\Splat.2.0.0\lib\Net45\Splat.dll True - - $(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\Squirrel.dll + + $(SolutionDir)\packages\squirrel.windows.1.7.5\lib\Net45\Squirrel.dll True @@ -190,6 +210,10 @@ {65dc628f-a640-4111-ab35-3a5652bc1e17} osu.Framework.Desktop + + {007B2356-AB6F-4BD9-96D5-116FC2DCE69A} + osu.Framework.Testing + {c76bf5b3-985e-4d39-95fe-97c9c879b83a} osu.Framework @@ -198,6 +222,10 @@ {d9a367c9-4c1a-489f-9b05-a0cea2b53b58} osu.Game.Resources + + {230ac4f3-7783-49fb-9aec-b83cda3b9f3d} + osu.Desktop.Tests + {c92a607b-1fdd-4954-9f92-03ff547d9080} osu.Game.Rulesets.Osu @@ -221,13 +249,13 @@ + - - + - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 0ed2a0ba6f..be670936fd 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Mania.Beatmaps.Patterns; using osu.Game.Rulesets.Mania.MathUtils; -using osu.Game.Database; using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy; using OpenTK; using osu.Game.Audio; @@ -29,12 +28,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps private Pattern lastPattern = new Pattern(); private FastRandom random; private Beatmap beatmap; - private bool isForCurrentRuleset; - protected override Beatmap ConvertBeatmap(Beatmap original, bool isForCurrentRuleset) + private readonly int availableColumns; + private readonly bool isForCurrentRuleset; + + public ManiaBeatmapConverter(bool isForCurrentRuleset, int availableColumns) { - this.isForCurrentRuleset = isForCurrentRuleset; + if (availableColumns <= 0) throw new ArgumentOutOfRangeException(nameof(availableColumns)); + this.isForCurrentRuleset = isForCurrentRuleset; + this.availableColumns = availableColumns; + } + + protected override Beatmap ConvertBeatmap(Beatmap original) + { beatmap = original; BeatmapDifficulty difficulty = original.BeatmapInfo.Difficulty; @@ -42,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps int seed = (int)Math.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)Math.Round(difficulty.ApproachRate); random = new FastRandom(seed); - return base.ConvertBeatmap(original, isForCurrentRuleset); + return base.ConvertBeatmap(original); } protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) @@ -90,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The hit objects generated. private IEnumerable generateSpecific(HitObject original) { - var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern); + var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, availableColumns, lastPattern); Pattern newPattern = generator.Generate(); lastPattern = newPattern; @@ -114,14 +121,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Patterns.PatternGenerator conversion = null; if (distanceData != null) - conversion = new DistanceObjectPatternGenerator(random, original, beatmap, lastPattern); + conversion = new DistanceObjectPatternGenerator(random, original, beatmap, availableColumns, lastPattern); else if (endTimeData != null) - conversion = new EndTimeObjectPatternGenerator(random, original, beatmap); + conversion = new EndTimeObjectPatternGenerator(random, original, beatmap, availableColumns); else if (positionData != null) { computeDensity(original.StartTime); - conversion = new HitObjectPatternGenerator(random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair); + conversion = new HitObjectPatternGenerator(random, original, beatmap, availableColumns, lastPattern, lastTime, lastPosition, density, lastStair); recordNote(original.StartTime, positionData.Position); } @@ -143,8 +150,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator { - public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern) - : base(random, hitObject, beatmap, previousPattern) + public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern) + : base(random, hitObject, beatmap, availableColumns, previousPattern) { } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 2d1f75e196..20966a75f7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -29,8 +29,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private PatternType convertType; - public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern) - : base(random, hitObject, beatmap, previousPattern) + public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern) + : base(random, hitObject, beatmap, availableColumns, previousPattern) { convertType = PatternType.None; if (Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy // The true distance, accounting for any repeats double distance = (distanceData?.Distance ?? 0) * repeatCount; // The velocity of the osu! hit object - calculated as the velocity of a slider - double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier / (timingPoint.BeatLength * difficultyPoint.SpeedMultiplier); + double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; @@ -448,7 +448,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return curveData.RepeatSamples[index]; } - /// /// Constructs and adds a note to a pattern. /// @@ -480,7 +479,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy Tail = { Samples = sampleInfoListAt(endTime) } }; - newObject = holdNote; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 6ad7489e0f..c353496410 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { private readonly double endTime; - public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap) - : base(random, hitObject, beatmap, new Pattern()) + public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns) + : base(random, hitObject, beatmap, availableColumns, new Pattern()) { var endtimeData = HitObject as IHasEndTime; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index b1ba99d98b..077b926635 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -20,9 +20,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private readonly PatternType convertType; - public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair) - : base(random, hitObject, beatmap, previousPattern) + public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair) + : base(random, hitObject, beatmap, availableColumns, previousPattern) { + if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime)); + if (density < 0) throw new ArgumentOutOfRangeException(nameof(density)); + StairType = lastStair; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); @@ -33,12 +36,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy float positionSeparation = ((positionData?.Position ?? Vector2.Zero) - previousPosition).Length; double timeSeparation = hitObject.StartTime - previousTime; - if (timeSeparation <= 125) - { - // More than 120 BPM - convertType |= PatternType.ForceNotStack; - } - if (timeSeparation <= 80) { // More than 187 BPM @@ -64,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy // More than 111 BPM stream convertType |= PatternType.Cycle | PatternType.KeepSingle; } - else if (timeSeparation <= 150 & positionSeparation < 20) + else if (timeSeparation <= 150 && positionSeparation < 20) { // More than 100 BPM stream convertType |= PatternType.ForceStack | PatternType.LowProbability; @@ -401,4 +398,4 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy }); } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index e6e3f1d07f..a3173f9784 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Objects; using OpenTK; @@ -26,11 +25,15 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// protected readonly FastRandom Random; - protected PatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern) - : base(hitObject, beatmap, previousPattern) + protected PatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern) + : base(hitObject, beatmap, availableColumns, previousPattern) { - Random = random; + if (random == null) throw new ArgumentNullException(nameof(random)); + if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); + if (availableColumns <= 0) throw new ArgumentOutOfRangeException(nameof(availableColumns)); + if (previousPattern == null) throw new ArgumentNullException(nameof(previousPattern)); + Random = random; RandomStart = AvailableColumns == 8 ? 1 : 0; } @@ -63,6 +66,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The amount of notes to be generated. protected int GetRandomNoteCount(double p2, double p3, double p4 = 0, double p5 = 0, double p6 = 0) { + if (p2 < 0 || p2 > 1) throw new ArgumentOutOfRangeException(nameof(p2)); + if (p3 < 0 || p3 > 1) throw new ArgumentOutOfRangeException(nameof(p3)); + if (p4 < 0 || p4 > 1) throw new ArgumentOutOfRangeException(nameof(p4)); + if (p5 < 0 || p5 > 1) throw new ArgumentOutOfRangeException(nameof(p5)); + if (p6 < 0 || p6 > 1) throw new ArgumentOutOfRangeException(nameof(p6)); + double val = Random.NextDouble(); if (val >= 1 - p6) return 6; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs index dda4d07182..ef321232c8 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs @@ -32,13 +32,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// protected readonly Beatmap Beatmap; - protected PatternGenerator(HitObject hitObject, Beatmap beatmap, Pattern previousPattern) + protected PatternGenerator(HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern) { - PreviousPattern = previousPattern; + if (hitObject == null) throw new ArgumentNullException(nameof(hitObject)); + if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); + if (availableColumns <= 0) throw new ArgumentOutOfRangeException(nameof(availableColumns)); + if (previousPattern == null) throw new ArgumentNullException(nameof(previousPattern)); + HitObject = hitObject; Beatmap = beatmap; - - AvailableColumns = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.CircleSize); + AvailableColumns = availableColumns; + PreviousPattern = previousPattern; } /// diff --git a/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs b/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs index 674d83f6f2..52b55a4ff5 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HitWindows.cs @@ -1,7 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Database; +using osu.Game.Beatmaps; namespace osu.Game.Rulesets.Mania.Judgements { diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index aaba4d94f0..1f01750f44 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -6,6 +6,7 @@ using osu.Game.Rulesets.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using System.Collections.Generic; +using System; namespace osu.Game.Rulesets.Mania { @@ -21,6 +22,6 @@ namespace osu.Game.Rulesets.Mania return 0; } - protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(true, (int)Math.Max(1, Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize))); } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs new file mode 100644 index 0000000000..b608e4d8d6 --- /dev/null +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.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 + +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania +{ + public class ManiaInputManager : RulesetInputManager + { + public ManiaInputManager(RulesetInfo ruleset) + : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + } + + public enum ManiaAction + { + // placeholder + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 30d1846746..c7809f83ed 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -2,13 +2,13 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Scoring; @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania { public class ManiaRuleset : Ruleset { - public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new ManiaHitRenderer(beatmap, isForCurrentRuleset); + public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new ManiaRulesetContainer(this, beatmap, isForCurrentRuleset); public override IEnumerable GetModsFor(ModType type) { @@ -27,7 +27,14 @@ namespace osu.Game.Rulesets.Mania { new ManiaModEasy(), new ManiaModNoFail(), - new ManiaModHalfTime(), + new MultiMod + { + Mods = new Mod[] + { + new ManiaModHalfTime(), + new ManiaModDaycore(), + }, + }, }; case ModType.DifficultyIncrease: @@ -89,6 +96,7 @@ namespace osu.Game.Rulesets.Mania new ModCinema(), }, }, + new ManiaModGravity() }; default: @@ -96,16 +104,21 @@ namespace osu.Game.Rulesets.Mania } } + public override Mod GetAutoplayMod() => new ModAutoplay(); + public override string Description => "osu!mania"; - public override FontAwesome Icon => FontAwesome.fa_osu_mania_o; - - public override IEnumerable CreateGameplayKeys() => new KeyCounter[] { /* Todo: Should be keymod specific */ }; + public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o }; public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new ManiaDifficultyCalculator(beatmap); public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); public override int LegacyID => 3; + + public ManiaRuleset(RulesetInfo rulesetInfo) + : base(rulesetInfo) + { + } } } diff --git a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs index ff3fd8e4b7..f31873d1c8 100644 --- a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs +++ b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs @@ -87,6 +87,5 @@ namespace osu.Game.Rulesets.Mania.MathUtils bitIndex++; return ((bitBuffer >>= 1) & 1) == 1; } - } } diff --git a/osu.Game.Rulesets.Mania/Mods/IGenerateSpeedAdjustments.cs b/osu.Game.Rulesets.Mania/Mods/IGenerateSpeedAdjustments.cs new file mode 100644 index 0000000000..954ee3f481 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/IGenerateSpeedAdjustments.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Timing; + +namespace osu.Game.Rulesets.Mania.Mods +{ + /// + /// A type of mod which generates speed adjustments that scroll the hit objects and bar lines. + /// + internal interface IGenerateSpeedAdjustments + { + /// + /// Applies this mod to a hit renderer. + /// + /// The hit renderer to apply to. + /// The per-column list of speed adjustments for hit objects. + /// The list of speed adjustments for bar lines. + void ApplyToRulesetContainer(ManiaRulesetContainer rulesetContainer, ref List[] hitObjectTimingChanges, ref List barlineTimingChanges); + } +} diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs index b402d3a010..f44ad6fd60 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs @@ -34,6 +34,11 @@ namespace osu.Game.Rulesets.Mania.Mods } + public class ManiaModDaycore : ModDaycore + { + public override double ScoreMultiplier => 0.3; + } + public class ManiaModDoubleTime : ModDoubleTime { public override double ScoreMultiplier => 1.0; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModGravity.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModGravity.cs new file mode 100644 index 0000000000..a054e0db56 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModGravity.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Timing; +using osu.Game.Rulesets.Timing; +using osu.Game.Rulesets.Mania.Objects.Drawables; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModGravity : Mod, IGenerateSpeedAdjustments + { + public override string Name => "Gravity"; + + public override double ScoreMultiplier => 0; + + public override FontAwesome Icon => FontAwesome.fa_sort_desc; + + public void ApplyToRulesetContainer(ManiaRulesetContainer rulesetContainer, ref List[] hitObjectTimingChanges, ref List barlineTimingChanges) + { + // We have to generate one speed adjustment per hit object for gravity + foreach (ManiaHitObject obj in rulesetContainer.Objects) + { + MultiplierControlPoint controlPoint = rulesetContainer.CreateControlPointAt(obj.StartTime); + // Beat length has too large of an effect for gravity, so we'll force it to a constant value for now + controlPoint.TimingPoint.BeatLength = 1000; + + hitObjectTimingChanges[obj.Column].Add(new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Gravity)); + } + + // Like with hit objects, we need to generate one speed adjustment per bar line + foreach (DrawableBarLine barLine in rulesetContainer.BarLines) + { + var controlPoint = rulesetContainer.CreateControlPointAt(barLine.HitObject.StartTime); + // Beat length has too large of an effect for gravity, so we'll force it to a constant value for now + controlPoint.TimingPoint.BeatLength = 1000; + + barlineTimingChanges.Add(new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Gravity)); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 0b4d8b2d4e..fc5ea4e116 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -3,7 +3,7 @@ using OpenTK; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Objects.Drawables diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 5d7f3314cd..17b0b0a607 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables RelativeSizeAxes = Axes.Both; Height = (float)HitObject.Duration; - Add(new Drawable[] + AddRange(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. @@ -55,7 +55,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables tickContainer = new Container { RelativeSizeAxes = Axes.Both, - RelativeCoordinateSpace = new Vector2(1, (float)HitObject.Duration) + RelativeChildOffset = new Vector2(0, (float)HitObject.StartTime), + RelativeChildSize = new Vector2(1, (float)HitObject.Duration) }, head = new DrawableHeadNote(this, key) { @@ -76,9 +77,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables HoldStartTime = () => holdStartTime }; - // To make the ticks relative to ourselves we need to offset them backwards - drawableTick.Y -= (float)HitObject.StartTime; - tickContainer.Add(drawableTick); AddNested(drawableTick); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index 9ecc77d3fc..39abbb6b3d 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -7,9 +7,9 @@ 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.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.AccentColour = value; - glowContainer.EdgeEffect = new EdgeEffect + glowContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Radius = 2f, diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 4e276fddb7..10dc607ec3 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -4,13 +4,12 @@ using OpenTK.Graphics; using OpenTK.Input; using osu.Framework.Configuration; -using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Objects.Drawables { - public abstract class DrawableManiaHitObject : DrawableHitObject + public abstract class DrawableManiaHitObject : DrawableScrollingHitObject where TObject : ManiaHitObject { /// @@ -27,9 +26,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (key != null) Key.BindTo(key); - - RelativePositionAxes = Axes.Y; - Y = (float)HitObject.StartTime; } public override Color4 AccentColour diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 658d409bb8..9322fed3eb 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public DrawableNote(Note hitObject, Bindable key = null) : base(hitObject, key) { - RelativeSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; Height = 100; Add(headPiece = new NotePiece diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs index c10aa9994b..04e8df4ae2 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs @@ -4,7 +4,7 @@ using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs index e01199e929..3df085c346 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs @@ -5,7 +5,7 @@ using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index fa32d46a88..c3a29b39a8 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -2,8 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Database; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Mania.Objects diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 6c0cacd277..3c4ff4216f 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.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.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Database; using osu.Game.Rulesets.Mania.Judgements; namespace osu.Game.Rulesets.Mania.Objects diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 798d4b8c5b..63b443319f 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -156,8 +155,8 @@ namespace osu.Game.Rulesets.Mania.Scoring { } - public ManiaScoreProcessor(HitRenderer hitRenderer) - : base(hitRenderer) + public ManiaScoreProcessor(RulesetContainer rulesetContainer) + : base(rulesetContainer) { } diff --git a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs b/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs deleted file mode 100644 index 0a8bc2d44a..0000000000 --- a/osu.Game.Rulesets.Mania/Timing/ControlPointContainer.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using OpenTK; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Objects; - -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 InvalidOperationException("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 TimingChange 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(TimingChange 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 - { - protected override IComparer DepthComparer => new HitObjectReverseStartTimeComparer(); - - 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/Timing/GravityScrollingContainer.cs b/osu.Game.Rulesets.Mania/Timing/GravityScrollingContainer.cs new file mode 100644 index 0000000000..699acc477b --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/GravityScrollingContainer.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Timing; + +namespace osu.Game.Rulesets.Mania.Timing +{ + /// + /// A that emulates a form of gravity where hit objects speed up over time. + /// + internal class GravityScrollingContainer : ScrollingContainer + { + private readonly MultiplierControlPoint controlPoint; + + public GravityScrollingContainer(MultiplierControlPoint controlPoint) + { + this.controlPoint = controlPoint; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // The gravity-adjusted start position + float startPos = (float)computeGravityTime(controlPoint.StartTime); + // The gravity-adjusted end position + float endPos = (float)computeGravityTime(controlPoint.StartTime + RelativeChildSize.Y); + + Y = startPos; + Height = endPos - startPos; + } + + /// + /// Applies gravity to a time value based on the current time. + /// + /// The time value gravity should be applied to. + /// The time after gravity is applied to . + private double computeGravityTime(double time) + { + double relativeTime = relativeTimeAt(time); + + // The sign of the relative time, this is used to apply backwards acceleration leading into startTime + double sign = relativeTime < 0 ? -1 : 1; + + return VisibleTimeRange - acceleration * relativeTime * relativeTime * sign; + } + + /// + /// The acceleration due to "gravity" of the content of this container. + /// + private double acceleration => 1 / VisibleTimeRange; + + /// + /// Computes the current time relative to , accounting for . + /// + /// The non-offset time. + /// The current time relative to - . + private double relativeTimeAt(double time) => Time.Current - time + VisibleTimeRange; + } +} diff --git a/osu.Game.Rulesets.Mania/Timing/ManiaSpeedAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/Timing/ManiaSpeedAdjustmentContainer.cs new file mode 100644 index 0000000000..321b4ee92b --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/ManiaSpeedAdjustmentContainer.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Timing; + +namespace osu.Game.Rulesets.Mania.Timing +{ + public class ManiaSpeedAdjustmentContainer : SpeedAdjustmentContainer + { + private readonly ScrollingAlgorithm scrollingAlgorithm; + + public ManiaSpeedAdjustmentContainer(MultiplierControlPoint timingSection, ScrollingAlgorithm scrollingAlgorithm) + : base(timingSection) + { + this.scrollingAlgorithm = scrollingAlgorithm; + } + + protected override ScrollingContainer CreateScrollingContainer() + { + switch (scrollingAlgorithm) + { + default: + return base.CreateScrollingContainer(); + case ScrollingAlgorithm.Gravity: + return new GravityScrollingContainer(ControlPoint); + } + } + } +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Timing/ScrollingAlgorithm.cs b/osu.Game.Rulesets.Mania/Timing/ScrollingAlgorithm.cs new file mode 100644 index 0000000000..72e096f5aa --- /dev/null +++ b/osu.Game.Rulesets.Mania/Timing/ScrollingAlgorithm.cs @@ -0,0 +1,17 @@ +// 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.Timing +{ + public enum ScrollingAlgorithm + { + /// + /// Basic scrolling algorithm based on the timing section time. This is the default algorithm. + /// + Basic, + /// + /// Emulating a form of gravity where hit objects speed up over time. + /// + Gravity + } +} diff --git a/osu.Game.Rulesets.Mania/Timing/TimingChange.cs b/osu.Game.Rulesets.Mania/Timing/TimingChange.cs deleted file mode 100644 index 9153ba6991..0000000000 --- a/osu.Game.Rulesets.Mania/Timing/TimingChange.cs +++ /dev/null @@ -1,23 +0,0 @@ -// 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.Timing -{ - public class TimingChange - { - /// - /// The time at which this timing change happened. - /// - public double Time; - - /// - /// The beat length. - /// - public double BeatLength = 500; - - /// - /// The speed multiplier. - /// - public double SpeedMultiplier = 1; - } -} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 6dfd5000d4..9fbc9ba5e7 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -7,21 +7,20 @@ 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.Shapes; 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 System; using osu.Framework.Configuration; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Judgements; namespace osu.Game.Rulesets.Mania.UI { - public class Column : Container, IHasAccentColour + public class Column : ScrollingPlayfield, IHasAccentColour { private const float key_icon_size = 10; private const float key_icon_corner_radius = 3; @@ -42,14 +41,15 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Container hitTargetBar; private readonly Container keyIcon; - public readonly ControlPointContainer ControlPointContainer; + protected override Container Content => content; + private readonly Container content; - public Column(IEnumerable timingChanges) + public Column() + : base(Axes.Y) { - RelativeSizeAxes = Axes.Y; Width = column_width; - Children = new Drawable[] + InternalChildren = new Drawable[] { background = new Box { @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.UI { Name = "Hit target + hit objects", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = ManiaPlayfield.HIT_TARGET_POSITION}, + Padding = new MarginPadding { Top = ManiaPlayfield.HIT_TARGET_POSITION }, Children = new Drawable[] { new Container @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.UI } } }, - ControlPointContainer = new ControlPointContainer(timingChanges) + content = new Container { Name = "Hit objects", RelativeSizeAxes = Axes.Both, @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Mania.UI { Name = "Key gradient", RelativeSizeAxes = Axes.Both, - ColourInfo = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)), + Colour = ColourInfo.GradientVertical(Color4.Black, Color4.Black.Opacity(0)), Alpha = 0.5f }, keyIcon = new Container @@ -145,6 +145,8 @@ namespace osu.Game.Rulesets.Mania.UI }; } + public override Axes RelativeSizeAxes => Axes.Y; + private bool isSpecial; public bool IsSpecial { @@ -171,14 +173,14 @@ namespace osu.Game.Rulesets.Mania.UI background.Colour = accentColour; - hitTargetBar.EdgeEffect = new EdgeEffect + hitTargetBar.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Radius = 5, Colour = accentColour.Opacity(0.5f), }; - keyIcon.EdgeEffect = new EdgeEffect + keyIcon.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Radius = 5, @@ -187,10 +189,14 @@ namespace osu.Game.Rulesets.Mania.UI } } - public void Add(DrawableHitObject hitObject) + /// + /// Adds a DrawableHitObject to this Playfield. + /// + /// The DrawableHitObject to add. + public override void Add(DrawableHitObject hitObject) { hitObject.AccentColour = AccentColour; - ControlPointContainer.Add(hitObject); + HitObjects.Add(hitObject); } private bool onKeyDown(InputState state, KeyDownEventArgs args) @@ -200,8 +206,8 @@ namespace osu.Game.Rulesets.Mania.UI if (args.Key == Key) { - background.FadeTo(background.Alpha + 0.2f, 50, EasingTypes.OutQuint); - keyIcon.ScaleTo(1.4f, 50, EasingTypes.OutQuint); + background.FadeTo(background.Alpha + 0.2f, 50, Easing.OutQuint); + keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint); } return false; @@ -211,8 +217,8 @@ namespace osu.Game.Rulesets.Mania.UI { if (args.Key == Key) { - background.FadeTo(0.2f, 800, EasingTypes.OutQuart); - keyIcon.ScaleTo(1f, 400, EasingTypes.OutQuart); + background.FadeTo(0.2f, 800, Easing.OutQuart); + keyIcon.ScaleTo(1f, 400, Easing.OutQuart); } return false; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs b/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs deleted file mode 100644 index 57477147d5..0000000000 --- a/osu.Game.Rulesets.Mania/UI/ManiaHitRenderer.cs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using OpenTK; -using OpenTK.Input; -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Lists; -using osu.Framework.MathUtils; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -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.Mania.Timing; -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 int? Columns; - - public ManiaHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(beatmap, isForCurrentRuleset) - { - } - - protected override Playfield CreatePlayfield() - { - double lastSpeedMultiplier = 1; - double lastBeatLength = 500; - - // Merge timing + difficulty points - var allPoints = new SortedList(Comparer.Default); - allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints); - allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints); - - // Generate the timing points, making non-timing changes use the previous timing change - var timingChanges = allPoints.Select(c => - { - var timingPoint = c as TimingControlPoint; - var difficultyPoint = c as DifficultyControlPoint; - - if (timingPoint != null) - lastBeatLength = timingPoint.BeatLength; - - if (difficultyPoint != null) - lastSpeedMultiplier = difficultyPoint.SpeedMultiplier; - - return new TimingChange - { - Time = c.Time, - BeatLength = lastBeatLength, - SpeedMultiplier = lastSpeedMultiplier - }; - }); - - double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; - - // Perform some post processing of the timing changes - timingChanges = timingChanges - // Collapse sections after the last hit object - .Where(s => s.Time <= lastObjectTime) - // Collapse sections with the same start time - .GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time) - // Collapse sections with the same beat length - .GroupBy(s => s.BeatLength * s.SpeedMultiplier).Select(g => g.First()) - .ToList(); - - return new ManiaPlayfield(Columns ?? (int)Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize), timingChanges) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - // Invert by default for now (should be moved to config/skin later) - Scale = new Vector2(1, -1) - }; - } - - [BackgroundDependencyLoader] - private void load() - { - var maniaPlayfield = (ManiaPlayfield)Playfield; - - double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; - - SortedList timingPoints = Beatmap.ControlPointInfo.TimingPoints; - for (int i = 0; i < timingPoints.Count; i++) - { - TimingControlPoint point = timingPoints[i]; - - // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object - double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - point.BeatLength : lastObjectTime + point.BeatLength * (int)point.TimeSignature; - - int index = 0; - for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++) - { - maniaPlayfield.Add(new DrawableBarLine(new BarLine - { - StartTime = t, - ControlPoint = point, - BeatIndex = index - })); - } - } - } - - public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); - - protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); - - protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) - { - var maniaPlayfield = Playfield as ManiaPlayfield; - if (maniaPlayfield == null) - return null; - - Bindable key = maniaPlayfield.Columns.ElementAt(h.Column).Key; - - var holdNote = h as HoldNote; - if (holdNote != null) - return new DrawableHoldNote(holdNote, key); - - var note = h as Note; - if (note != null) - return new DrawableNote(note, key); - - 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 2e6b63579e..0c9351cad2 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.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.Sprites; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.UI; using OpenTK; @@ -15,25 +14,16 @@ 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.Framework.Graphics.Transforms; -using osu.Framework.MathUtils; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Mania.UI { - public class ManiaPlayfield : Playfield + public class ManiaPlayfield : ScrollingPlayfield { 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... @@ -58,21 +48,23 @@ namespace osu.Game.Rulesets.Mania.UI private readonly FlowContainer columns; public IEnumerable Columns => columns.Children; - private readonly ControlPointContainer barLineContainer; + protected override Container Content => content; + private readonly Container content; private List normalColumnColours = new List(); private Color4 specialColumnColour; private readonly int columnCount; - public ManiaPlayfield(int columnCount, IEnumerable timingChanges) + public ManiaPlayfield(int columnCount) + : base(Axes.Y) { this.columnCount = columnCount; if (columnCount <= 0) throw new ArgumentException("Can't have zero or fewer columns."); - Children = new Drawable[] + InternalChildren = new Drawable[] { new Container { @@ -116,12 +108,12 @@ namespace osu.Game.Rulesets.Mania.UI Padding = new MarginPadding { Top = HIT_TARGET_POSITION }, Children = new[] { - barLineContainer = new ControlPointContainer(timingChanges) + content = new Container { Name = "Bar lines", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y + RelativeSizeAxes = Axes.Y, // Width is set in the Update method } } @@ -131,9 +123,14 @@ namespace osu.Game.Rulesets.Mania.UI }; for (int i = 0; i < columnCount; i++) - columns.Add(new Column(timingChanges)); + { + var c = new Column(); + c.Reversed.BindTo(Reversed); + c.VisibleTimeRange.BindTo(VisibleTimeRange); - TimeSpan = time_span_default; + columns.Add(c); + AddNested(c); + } } [BackgroundDependencyLoader] @@ -207,79 +204,13 @@ namespace osu.Game.Rulesets.Mania.UI } public override void Add(DrawableHitObject h) => Columns.ElementAt(h.HitObject.Column).Add(h); - public void Add(DrawableBarLine barline) => barLineContainer.Add(barline); - - 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.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()); - } + public void Add(DrawableBarLine barline) => HitObjects.Add(barline); protected override void Update() { // Due to masking differences, it is not possible to get the width of the columns container automatically // While masking on effectively only the Y-axis, so we need to set the width of the bar line container manually - barLineContainer.Width = columns.Width; - } - - 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; - } + content.Width = columns.Width; } } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs new file mode 100644 index 0000000000..0e750a348e --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -0,0 +1,129 @@ +// 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 OpenTK; +using OpenTK.Input; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Framework.Lists; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +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.Mania.Timing; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Timing; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class ManiaRulesetContainer : ScrollingRulesetContainer + { + /// + /// The number of columns which the should display, and which + /// the beatmap converter will attempt to convert beatmaps to use. + /// + private int availableColumns; + + public IEnumerable BarLines; + + public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(ruleset, beatmap, isForCurrentRuleset) + { + // Generate the bar lines + double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; + + SortedList timingPoints = Beatmap.ControlPointInfo.TimingPoints; + var barLines = new List(); + + for (int i = 0; i < timingPoints.Count; i++) + { + TimingControlPoint point = timingPoints[i]; + + // Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object + double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - point.BeatLength : lastObjectTime + point.BeatLength * (int)point.TimeSignature; + + int index = 0; + for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++) + { + barLines.Add(new DrawableBarLine(new BarLine + { + StartTime = t, + ControlPoint = point, + BeatIndex = index + })); + } + } + + BarLines = barLines; + } + + [BackgroundDependencyLoader] + private void load() + { + BarLines.ForEach(Playfield.Add); + } + + protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(availableColumns) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); + + public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo); + + protected override BeatmapConverter CreateBeatmapConverter() + { + if (IsForCurrentRuleset) + availableColumns = (int)Math.Max(1, Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.CircleSize)); + else + { + float percentSliderOrSpinner = (float)WorkingBeatmap.Beatmap.HitObjects.Count(h => h is IHasEndTime) / WorkingBeatmap.Beatmap.HitObjects.Count; + if (percentSliderOrSpinner < 0.2) + availableColumns = 7; + else if (percentSliderOrSpinner < 0.3 || Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.CircleSize) >= 5) + availableColumns = Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty) > 5 ? 7 : 6; + else if (percentSliderOrSpinner > 0.6) + availableColumns = Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty) > 4 ? 5 : 4; + else + availableColumns = Math.Max(4, Math.Min((int)Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty) + 1, 7)); + } + + return new ManiaBeatmapConverter(IsForCurrentRuleset, availableColumns); + } + + protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) + { + Bindable key = Playfield.Columns.ElementAt(h.Column).Key; + + var holdNote = h as HoldNote; + if (holdNote != null) + return new DrawableHoldNote(holdNote, key); + + var note = h as Note; + if (note != null) + return new DrawableNote(note, key); + + return null; + } + + protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(1, 0.8f); + + protected override SpeedAdjustmentContainer CreateSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) => new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Basic); + } +} diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 3d5614bd90..890c9116cf 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -33,8 +33,9 @@ false - - $(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll + + $(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll + True @@ -62,6 +63,7 @@ + @@ -77,14 +79,17 @@ + + + - + + - - + diff --git a/osu.Game.Rulesets.Mania/packages.config b/osu.Game.Rulesets.Mania/packages.config index 634d0b51f6..8add43d5d5 100644 --- a/osu.Game.Rulesets.Mania/packages.config +++ b/osu.Game.Rulesets.Mania/packages.config @@ -1,9 +1,8 @@  - - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs index cc06946d38..3b0cfc1ef1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs @@ -39,6 +39,11 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray(); } + public class OsuModDaycore : ModDaycore + { + public override double ScoreMultiplier => 0.5; + } + public class OsuModDoubleTime : ModDoubleTime { public override double ScoreMultiplier => 1.12; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index 9f8ff17853..30e6172802 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -6,7 +6,7 @@ using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections Masking = true; AutoSizeAxes = Axes.Both; CornerRadius = width / 2; - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = Color4.White.Opacity(0.2f), diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 925767b851..2396e5d129 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -94,12 +94,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections using (fp.BeginAbsoluteSequence(fadeInTime)) { fp.FadeIn(DrawableOsuHitObject.TIME_FADEIN); - fp.ScaleTo(1, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out); + fp.ScaleTo(1, DrawableOsuHitObject.TIME_FADEIN, Easing.Out); - fp.MoveTo(pointEndPosition, DrawableOsuHitObject.TIME_FADEIN, EasingTypes.Out); + fp.MoveTo(pointEndPosition, DrawableOsuHitObject.TIME_FADEIN, Easing.Out); - fp.Delay(fadeOutTime - fadeInTime); - fp.FadeOut(DrawableOsuHitObject.TIME_FADEIN); + fp.Delay(fadeOutTime - fadeInTime).FadeOut(DrawableOsuHitObject.TIME_FADEIN); } fp.Expire(true); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 09bfffeefe..f68a7a765b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -89,11 +89,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateInitialState(); - //sane defaults - ring.Alpha = circle.Alpha = number.Alpha = glow.Alpha = 1; - ApproachCircle.Alpha = 0; - ApproachCircle.Scale = new Vector2(4); - explode.Alpha = 0; + // sane defaults + ring.Show(); + circle.Show(); + number.Show(); + glow.Show(); + + ApproachCircle.Hide(); + ApproachCircle.ScaleTo(new Vector2(4)); + explode.Hide(); } protected override void UpdatePreemptState() @@ -106,43 +110,42 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdateCurrentState(ArmedState state) { - ApproachCircle.FadeOut(); + double duration = ((HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime) - HitObject.StartTime; - double endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; - double duration = endTime - HitObject.StartTime; - - glow.Delay(duration); - glow.FadeOut(400); + glow.Delay(duration).FadeOut(400); switch (state) { case ArmedState.Idle: - Delay(duration + TIME_PREEMPT); - FadeOut(TIME_FADEOUT); + this.Delay(duration + TIME_PREEMPT).FadeOut(TIME_FADEOUT); Expire(true); break; case ArmedState.Miss: - FadeOut(TIME_FADEOUT / 5); + ApproachCircle.FadeOut(50); + this.FadeOut(TIME_FADEOUT / 5); Expire(); break; case ArmedState.Hit: - const double flash_in = 40; + ApproachCircle.FadeOut(50); - flash.FadeTo(0.8f, flash_in); - flash.Delay(flash_in); - flash.FadeOut(100); + const double flash_in = 40; + flash.FadeTo(0.8f, flash_in) + .Then() + .FadeOut(100); explode.FadeIn(flash_in); - Delay(flash_in, true); + using (BeginDelayedSequence(flash_in, true)) + { + //after the flash, we can hide some elements that were behind it + ring.FadeOut(); + circle.FadeOut(); + number.FadeOut(); - //after the flash, we can hide some elements that were behind it - ring.FadeOut(); - circle.FadeOut(); - number.FadeOut(); + this.FadeOut(800) + .ScaleTo(Scale * 1.5f, 400, Easing.OutQuad); + } - FadeOut(800); - ScaleTo(Scale * 1.5f, 400, EasingTypes.OutQuad); Expire(); break; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 57a9804330..b3043d18f6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; +using osu.Framework.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -17,18 +18,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables : base(hitObject) { AccentColour = HitObject.ComboColour; + Alpha = 0; } protected override OsuJudgement CreateJudgement() => new OsuJudgement { MaxScore = OsuScoreResult.Hit300 }; protected sealed override void UpdateState(ArmedState state) { - Flush(); - - UpdateInitialState(); + FinishTransforms(); using (BeginAbsoluteSequence(HitObject.StartTime - TIME_PREEMPT, true)) { + UpdateInitialState(); + UpdatePreemptState(); using (BeginDelayedSequence(TIME_PREEMPT + Judgement.TimeOffset, true)) @@ -36,19 +38,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected virtual void UpdateCurrentState(ArmedState state) + protected virtual void UpdateInitialState() { + Hide(); } protected virtual void UpdatePreemptState() { - FadeIn(TIME_FADEIN); + this.FadeIn(TIME_FADEIN); } - protected virtual void UpdateInitialState() + protected virtual void UpdateCurrentState(ArmedState state) { - Alpha = 0; } + + private OsuInputManager osuActionInputManager; + internal OsuInputManager OsuActionInputManager => osuActionInputManager ?? (osuActionInputManager = GetContainingInputManager() as OsuInputManager); } public enum ComboResult diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index eaa0bb7d27..892d106b02 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void LoadComplete() { if (Judgement.Result != HitResult.Miss) - JudgementText.TransformSpacingTo(new Vector2(14, 0), 1800, EasingTypes.OutQuint); + JudgementText.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); base.LoadComplete(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index b80f1d7178..d5583b0d9d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -28,10 +28,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSlider(Slider s) : base(s) { - // Since the DrawableSlider itself is just a container without a size we need to - // pass all input through. - AlwaysReceiveInput = true; - SliderBouncer bouncer1; slider = s; @@ -129,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { if (!userTriggered && Time.Current >= slider.EndTime) { - var ticksCount = ticks.Children.Count() + 1; + var ticksCount = ticks.Children.Count + 1; var ticksHit = ticks.Children.Count(t => t.Judgement.Result == HitResult.Hit); if (initialCircle.Judgement.Result == HitResult.Hit) ticksHit++; @@ -162,14 +158,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { ball.FadeIn(); - Delay(slider.Duration, true); + using (BeginDelayedSequence(slider.Duration, true)) + { + body.FadeOut(160); + ball.FadeOut(160); - body.FadeOut(160); - ball.FadeOut(160); - - FadeOut(800); - - Expire(); + this.FadeOut(800) + .Expire(); + } } public Drawable ProxiedLayer => initialCircle.ApproachCircle; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 6b4d40e080..2a50b23047 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -3,11 +3,11 @@ using System; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -62,14 +62,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { var animIn = Math.Min(150, sliderTick.StartTime - FadeInTime); - ScaleTo(0.5f); - ScaleTo(1.2f, animIn); - FadeIn(animIn); - - Delay(animIn); - ScaleTo(1, 150, EasingTypes.Out); - - Delay(-animIn); + this.Animate( + d => d.FadeIn(animIn), + d => d.ScaleTo(0.5f).ScaleTo(1.2f, animIn) + ).Then( + d => d.ScaleTo(1, 150, Easing.Out) + ); } protected override void UpdateCurrentState(ArmedState state) @@ -77,16 +75,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables switch (state) { case ArmedState.Idle: - Delay(FadeOutTime - sliderTick.StartTime); - FadeOut(); + this.Delay(FadeOutTime - sliderTick.StartTime).FadeOut(); break; case ArmedState.Miss: - FadeOut(160); - FadeColour(Color4.Red, 80); + this.FadeOut(160) + .FadeColour(Color4.Red, 80); break; case ArmedState.Hit: - FadeOut(120, EasingTypes.OutQuint); - ScaleTo(Scale * 1.5f, 120, EasingTypes.OutQuint); + this.FadeOut(120, Easing.OutQuint) + .ScaleTo(Scale * 1.5f, 120, Easing.OutQuint); break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 3722d13ffc..8473cc2453 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly CirclePiece circle; private readonly GlowPiece glow; - private readonly TextAwesome symbol; + private readonly SpriteIcon symbol; private readonly Color4 baseColour = OsuColour.FromHex(@"002c3c"); private readonly Color4 fillColour = OsuColour.FromHex(@"005b7c"); @@ -38,8 +39,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSpinner(Spinner s) : base(s) { - AlwaysReceiveInput = true; - Origin = Anchor.Centre; Position = s.Position; @@ -66,12 +65,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre, }, new RingPiece(), - symbol = new TextAwesome + symbol = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - UseFullGlyphHeight = true, - TextSize = 48, + Size = new Vector2(48), Icon = FontAwesome.fa_asterisk, Shadow = false, }, @@ -168,6 +166,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables glow.Colour = colours.BlueDark; } + protected override void Update() + { + disc.Tracking = OsuActionInputManager.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton); + + base.Update(); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -176,9 +181,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ticks.Rotation = disc.Rotation; float relativeCircleScale = spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; - disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, EasingTypes.OutQuint); + disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); - symbol.RotateTo(disc.Rotation / 2, 500, EasingTypes.OutQuint); + symbol.RotateTo(disc.Rotation / 2, 500, Easing.OutQuint); } protected override void UpdatePreemptState() @@ -186,35 +191,33 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.UpdatePreemptState(); circleContainer.ScaleTo(spinner.Scale * 0.3f); - circleContainer.ScaleTo(spinner.Scale, TIME_PREEMPT / 1.4f, EasingTypes.OutQuint); + circleContainer.ScaleTo(spinner.Scale, TIME_PREEMPT / 1.4f, Easing.OutQuint); disc.RotateTo(-720); symbol.RotateTo(-720); - mainContainer.ScaleTo(0); - mainContainer.ScaleTo(spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, TIME_PREEMPT - 150, EasingTypes.OutQuint); - - mainContainer.Delay(TIME_PREEMPT - 150); - mainContainer.ScaleTo(1, 500, EasingTypes.OutQuint); + mainContainer + .ScaleTo(0) + .ScaleTo(spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, TIME_PREEMPT - 150, Easing.OutQuint) + .Then() + .ScaleTo(1, 500, Easing.OutQuint); } protected override void UpdateCurrentState(ArmedState state) { - Delay(spinner.Duration, true); - - FadeOut(160); + var sequence = this.Delay(spinner.Duration).FadeOut(160); switch (state) { case ArmedState.Hit: - ScaleTo(Scale * 1.2f, 320, EasingTypes.Out); - Expire(); + sequence.ScaleTo(Scale * 1.2f, 320, Easing.Out); break; case ArmedState.Miss: - ScaleTo(Scale * 0.8f, 320, EasingTypes.In); - Expire(); + sequence.ScaleTo(Scale * 0.8f, 320, Easing.In); break; } + + Expire(); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index 3004dafda7..0c3c4f0a6d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -7,12 +7,12 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Input; +using osu.Framework.Input.Bindings; using OpenTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class CirclePiece : Container + public class CirclePiece : Container, IKeyBindingHandler { private readonly Sprite disc; @@ -49,9 +49,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces disc.Texture = textures.Get(@"Play/osu/disc"); } - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + public bool OnPressed(OsuAction action) { - return Hit?.Invoke() ?? false; + switch (action) + { + case OsuAction.LeftButton: + case OsuAction.RightButton: + return IsHovered && (Hit?.Invoke() ?? false); + } + + return false; } + + public bool OnReleased(OsuAction action) => false; } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs index 68ffb756d4..97d7de35cf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs @@ -3,8 +3,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using OpenTK; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs index 07b21657a5..28e54c3b4e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Masking = true, Origin = Anchor.Centre, - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Radius = 60, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs index a04d3e7a0a..f66099e1c3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs @@ -3,9 +3,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 4cffc1def3..0bcb8a4986 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -1,10 +1,12 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using OpenTK; using OpenTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces @@ -96,18 +98,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return base.OnMouseMove(state); } + // If the current time is between the start and end of the slider, we should track mouse input regardless of the cursor position. + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => canCurrentlyTrack || base.ReceiveMouseInputAt(screenSpacePos); + private bool tracking; public bool Tracking { get { return tracking; } - set + private set { if (value == tracking) return; tracking = value; - follow.ScaleTo(tracking ? 2.8f : 1, 300, EasingTypes.OutQuint); - follow.FadeTo(tracking ? 0.2f : 0, 300, EasingTypes.OutQuint); + follow.ScaleTo(tracking ? 2.8f : 1, 300, Easing.OutQuint); + follow.FadeTo(tracking ? 0.2f : 0, 300, Easing.OutQuint); } } @@ -117,8 +122,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.Update(); + // Make sure to use the base version of ReceiveMouseInputAt so that we correctly check the position. if (Time.Current < slider.EndTime) - Tracking = canCurrentlyTrack && lastState != null && Contains(lastState.Mouse.NativeState.Position) && lastState.Mouse.HasMainButtonPressed; + Tracking = canCurrentlyTrack && lastState != null && base.ReceiveMouseInputAt(lastState.Mouse.NativeState.Position) && ((Parent as DrawableSlider)?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); } public void UpdateProgress(double progress, int repeat) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 8b9441ea65..680f987274 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; accentColour = value; - if (LoadState == LoadState.Loaded) + if (LoadState == LoadState.Ready) Schedule(reloadTexture); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBouncer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBouncer.cs index 65679dd7d3..942f166241 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBouncer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBouncer.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; +using OpenTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -11,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { private readonly Slider slider; private readonly bool isEnd; - private readonly TextAwesome icon; + private readonly SpriteIcon icon; public SliderBouncer(Slider slider, bool isEnd) { @@ -24,12 +25,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Children = new Drawable[] { - icon = new TextAwesome + icon = new SpriteIcon { Icon = FontAwesome.fa_eercast, Anchor = Anchor.Centre, Origin = Anchor.Centre, - TextSize = 48, + Size = new Vector2(48), } }; } @@ -37,8 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected override void LoadComplete() { base.LoadComplete(); - icon.RotateTo(360, 1000); - icon.Loop(); + icon.Spin(1000, RotationDirection.Clockwise); } public void UpdateProgress(double progress, int repeat) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs index 66cf7758b9..bdd5b71211 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs @@ -4,7 +4,7 @@ using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Disc.Colour = value; - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Hollow = true, Type = EdgeEffectType.Glow, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 29d6d1f147..6577c7fd50 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Game.Graphics; using OpenTK; @@ -31,7 +30,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { spinner = s; - AlwaysReceiveInput = true; RelativeSizeAxes = Axes.Both; Children = new Drawable[] @@ -40,6 +38,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces }; } + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; + private bool tracking; public bool Tracking { @@ -66,21 +66,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - Tracking |= state.Mouse.HasMainButtonPressed; - return base.OnMouseDown(state, args); - } - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) - { - Tracking &= state.Mouse.HasMainButtonPressed; - return base.OnMouseUp(state, args); - } - protected override bool OnMouseMove(InputState state) { - Tracking |= state.Mouse.HasMainButtonPressed; mousePosition = Parent.ToLocalSpace(state.Mouse.NativeState.Position); return base.OnMouseMove(state); } @@ -126,13 +113,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (Complete && updateCompleteTick()) { - background.Flush(flushType: typeof(TransformAlpha)); - background.FadeTo(tracking_alpha + 0.2f, 60, EasingTypes.OutExpo); - background.Delay(60); - background.FadeTo(tracking_alpha, 250, EasingTypes.OutQuint); + background.FinishTransforms(false, nameof(Alpha)); + background + .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo) + .Then() + .FadeTo(tracking_alpha, 250, Easing.OutQuint); } - RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, EasingTypes.OutExpo); + this.RotateTo(currentRotation / 2, validAndTracking ? 500 : 1500, Easing.OutExpo); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs index 4dbb6bd4d6..fc0d436788 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs @@ -5,9 +5,9 @@ using System; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Colour = Color4.Black, Alpha = 0.4f, - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Radius = 10, diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index e6fd82e6c8..7a311f1467 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -1,12 +1,12 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using OpenTK; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using OpenTK.Graphics; -using osu.Game.Database; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Osu.Objects diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObjectDifficulty.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObjectDifficulty.cs deleted file mode 100644 index 1786771dca..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObjectDifficulty.cs +++ /dev/null @@ -1,201 +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 System; -using System.Diagnostics; -using System.Linq; - -namespace osu.Game.Rulesets.Osu.Objects -{ - internal class OsuHitObjectDifficulty - { - /// - /// Factor by how much speed / aim strain decays per second. - /// - /// - /// These values are results of tweaking a lot and taking into account general feedback. - /// Opinionated observation: Speed is easier to maintain than accurate jumps. - /// - internal static readonly double[] DECAY_BASE = { 0.3, 0.15 }; - - /// - /// Pseudo threshold values to distinguish between "singles" and "streams" - /// - /// - /// Of course the border can not be defined clearly, therefore the algorithm has a smooth transition between those values. - /// They also are based on tweaking and general feedback. - /// - private const double stream_spacing_threshold = 110, - single_spacing_threshold = 125; - - /// - /// Scaling values for weightings to keep aim and speed difficulty in balance. - /// - /// - /// Found from testing a very large map pool (containing all ranked maps) and keeping the average values the same. - /// - private static readonly double[] spacing_weight_scaling = { 1400, 26.25 }; - - /// - /// Almost the normed diameter of a circle (104 osu pixel). That is -after- position transforming. - /// - private const double almost_diameter = 90; - - internal OsuHitObject BaseHitObject; - internal double[] Strains = { 1, 1 }; - - internal int MaxCombo = 1; - - private readonly float scalingFactor; - private float lazySliderLength; - - private readonly Vector2 startPosition; - private readonly Vector2 endPosition; - - internal OsuHitObjectDifficulty(OsuHitObject baseHitObject) - { - BaseHitObject = baseHitObject; - float circleRadius = baseHitObject.Scale * 64; - - Slider slider = BaseHitObject as Slider; - if (slider != null) - MaxCombo += slider.Ticks.Count(); - - // We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps. - scalingFactor = 52.0f / circleRadius; - if (circleRadius < 30) - { - float smallCircleBonus = Math.Min(30.0f - circleRadius, 5.0f) / 50.0f; - scalingFactor *= 1.0f + smallCircleBonus; - } - - lazySliderLength = 0; - startPosition = baseHitObject.StackedPosition; - - // Calculate approximation of lazy movement on the slider - if (slider != null) - { - float sliderFollowCircleRadius = circleRadius * 3; // Not sure if this is correct, but here we do not need 100% exact values. This comes pretty darn close in my tests. - - // For simplifying this step we use actual osu! coordinates and simply scale the length, that we obtain by the ScalingFactor later - Vector2 cursorPos = startPosition; - - Action addSliderVertex = delegate (Vector2 pos) - { - Vector2 difference = pos - cursorPos; - float distance = difference.Length; - - // Did we move away too far? - if (distance > sliderFollowCircleRadius) - { - // Yep, we need to move the cursor - difference.Normalize(); // Obtain the direction of difference. We do no longer need the actual difference - distance -= sliderFollowCircleRadius; - cursorPos += difference * distance; // We move the cursor just as far as needed to stay in the follow circle - lazySliderLength += distance; - } - }; - - // Actual computation of the first lazy curve - foreach (var tick in slider.Ticks) - addSliderVertex(tick.StackedPosition); - - addSliderVertex(baseHitObject.StackedEndPosition); - - lazySliderLength *= scalingFactor; - endPosition = cursorPos; - } - // We have a normal HitCircle or a spinner - else - endPosition = startPosition; - } - - internal void CalculateStrains(OsuHitObjectDifficulty previousHitObject, double timeRate) - { - calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Speed, timeRate); - calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Aim, timeRate); - } - - // Caution: The subjective values are strong with this one - private static double spacingWeight(double distance, OsuDifficultyCalculator.DifficultyType type) - { - switch (type) - { - case OsuDifficultyCalculator.DifficultyType.Speed: - if (distance > single_spacing_threshold) - return 2.5; - else if (distance > stream_spacing_threshold) - return 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold); - else if (distance > almost_diameter) - return 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter); - else if (distance > almost_diameter / 2) - return 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2); - else - return 0.95; - - case OsuDifficultyCalculator.DifficultyType.Aim: - return Math.Pow(distance, 0.99); - } - - Debug.Assert(false, "Invalid osu difficulty hit object type."); - return 0; - } - - private void calculateSpecificStrain(OsuHitObjectDifficulty previousHitObject, OsuDifficultyCalculator.DifficultyType type, double timeRate) - { - double addition = 0; - double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate; - double decay = Math.Pow(DECAY_BASE[(int)type], timeElapsed / 1000); - - if (BaseHitObject is Spinner) - { - // Do nothing for spinners - } - else if (BaseHitObject is Slider) - { - switch (type) - { - case OsuDifficultyCalculator.DifficultyType.Speed: - - // For speed strain we treat the whole slider as a single spacing entity, since "Speed" is about how hard it is to click buttons fast. - // The spacing weight exists to differentiate between being able to easily alternate or having to single. - addition = - spacingWeight(previousHitObject.lazySliderLength + - DistanceTo(previousHitObject), type) * - spacing_weight_scaling[(int)type]; - - break; - case OsuDifficultyCalculator.DifficultyType.Aim: - - // For Aim strain we treat each slider segment and the jump after the end of the slider as separate jumps, since movement-wise there is no difference - // to multiple jumps. - addition = - ( - spacingWeight(previousHitObject.lazySliderLength, type) + - spacingWeight(DistanceTo(previousHitObject), type) - ) * - spacing_weight_scaling[(int)type]; - - break; - } - } - else if (BaseHitObject is HitCircle) - { - addition = spacingWeight(DistanceTo(previousHitObject), type) * spacing_weight_scaling[(int)type]; - } - - // Scale addition by the time, that elapsed. Filter out HitObjects that are too close to be played anyway to avoid crazy values by division through close to zero. - // You will never find maps that require this amongst ranked maps. - addition /= Math.Max(timeElapsed, 50); - - Strains[(int)type] = previousHitObject.Strains[(int)type] * decay + addition; - } - - internal double DistanceTo(OsuHitObjectDifficulty other) - { - // Scale the distance by circle size. - return (startPosition - other.endPosition).Length * scalingFactor; - } - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 3b44e38d5e..056bde4005 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -6,9 +6,9 @@ using osu.Game.Rulesets.Objects.Types; using System; using System.Collections.Generic; using osu.Game.Rulesets.Objects; -using osu.Game.Database; using System.Linq; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Osu.Objects @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier / difficultyPoint.SpeedMultiplier; + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate; diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 6ba499739a..c4f5dfe97a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.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.Game.Beatmaps; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Database; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Osu.Objects diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs new file mode 100644 index 0000000000..a164566263 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs @@ -0,0 +1,73 @@ +// 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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Beatmaps; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; +using osu.Game.Rulesets.Osu.OsuDifficulty.Skills; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty +{ + public class OsuDifficultyCalculator : DifficultyCalculator + { + private const int section_length = 400; + private const double difficulty_multiplier = 0.0675; + + public OsuDifficultyCalculator(Beatmap beatmap) : base(beatmap) + { + } + + protected override void PreprocessHitObjects() + { + foreach (OsuHitObject h in Objects) + (h as Slider)?.Curve?.Calculate(); + } + + protected override double CalculateInternal(Dictionary categoryDifficulty) + { + OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Objects); + Skill[] skills = + { + new Aim(), + new Speed() + }; + + double sectionEnd = section_length / TimeRate; + foreach (OsuDifficultyHitObject h in beatmap) + { + while (h.BaseObject.StartTime > sectionEnd) + { + foreach (Skill s in skills) + { + s.SaveCurrentPeak(); + s.StartNewSectionFrom(sectionEnd); + } + + sectionEnd += section_length; + } + + foreach (Skill s in skills) + s.Process(h); + } + + double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; + double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; + + double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2; + + if (categoryDifficulty != null) + { + categoryDifficulty.Add("Aim", aimRating.ToString("0.00")); + categoryDifficulty.Add("Speed", speedRating.ToString("0.00")); + } + + return starRating; + } + + protected override BeatmapConverter CreateBeatmapConverter() => new OsuBeatmapConverter(); + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs new file mode 100644 index 0000000000..72ba421344 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections; +using System.Collections.Generic; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing +{ + /// + /// An enumerable container wrapping input as + /// which contains extra data required for difficulty calculation. + /// + public class OsuDifficultyBeatmap : IEnumerable + { + private readonly IEnumerator difficultyObjects; + private readonly Queue onScreen = new Queue(); + + /// + /// Creates an enumerator, which preprocesses a list of s recieved as input, wrapping them as + /// which contains extra data required for difficulty calculation. + /// + public OsuDifficultyBeatmap(List objects) + { + // Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases. + // This should probably happen before the objects reach the difficulty calculator. + objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime)); + difficultyObjects = createDifficultyObjectEnumerator(objects); + } + + /// + /// Returns an enumerator that enumerates all s in the . + /// The inner loop adds objects that appear on screen into a queue until we need to hit the next object. + /// The outer loop returns objects from this queue one at a time, only after they had to be hit, and should no longer be on screen. + /// This means that we can loop through every object that is on screen at the time when a new one appears, + /// allowing us to determine a reading strain for the object that just appeared. + /// + public IEnumerator GetEnumerator() + { + while (true) + { + // Add upcoming objects to the queue until we have at least one object that had been hit and can be dequeued. + // This means there is always at least one object in the queue unless we reached the end of the map. + do + { + if (!difficultyObjects.MoveNext()) + break; // New objects can't be added anymore, but we still need to dequeue and return the ones already on screen. + + OsuDifficultyHitObject latest = difficultyObjects.Current; + // Calculate flow values here + + foreach (OsuDifficultyHitObject h in onScreen) + { + h.TimeUntilHit -= latest.DeltaTime; + // Calculate reading strain here + } + + onScreen.Enqueue(latest); + } + while (onScreen.Peek().TimeUntilHit > 0); // Keep adding new objects on screen while there is still time before we have to hit the next one. + + if (onScreen.Count == 0) break; // We have reached the end of the map and enumerated all the objects. + yield return onScreen.Dequeue(); // Remove and return objects one by one that had to be hit before the latest one appeared. + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private IEnumerator createDifficultyObjectEnumerator(List objects) + { + // We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object. + OsuHitObject[] triangle = new OsuHitObject[3]; + + // OsuDifficultyHitObject construction requires three components, an extra copy of the first OsuHitObject is used at the beginning. + if (objects.Count > 1) + { + triangle[1] = objects[0]; // This copy will get shifted to the last spot in the triangle. + triangle[0] = objects[0]; // This component corresponds to the real first OsuHitOject. + } + + // The final component of the first triangle will be the second OsuHitOject of the map, which forms the first jump. + // If the map has less than two OsuHitObjects, the enumerator will not return anything. + for (int i = 1; i < objects.Count; ++i) + { + triangle[2] = triangle[1]; + triangle[1] = triangle[0]; + triangle[0] = objects[i]; + + yield return new OsuDifficultyHitObject(triangle); + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs new file mode 100644 index 0000000000..bdeb62df3e --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing +{ + /// + /// A wrapper around extending it with additional data required for difficulty calculation. + /// + public class OsuDifficultyHitObject + { + /// + /// The this refers to. + /// + public OsuHitObject BaseObject { get; } + + /// + /// Normalized distance from the of the previous . + /// + public double Distance { get; private set; } + + /// + /// Milliseconds elapsed since the StartTime of the previous . + /// + public double DeltaTime { get; private set; } + + /// + /// Number of milliseconds until the has to be hit. + /// + public double TimeUntilHit { get; set; } + + private const int normalized_radius = 52; + + private readonly OsuHitObject[] t; + + /// + /// Initializes the object calculating extra data required for difficulty calculation. + /// + public OsuDifficultyHitObject(OsuHitObject[] triangle) + { + t = triangle; + BaseObject = t[0]; + setDistances(); + setTimingValues(); + // Calculate angle here + } + + private void setDistances() + { + // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. + double scalingFactor = normalized_radius / BaseObject.Radius; + if (BaseObject.Radius < 30) + { + double smallCircleBonus = Math.Min(30 - BaseObject.Radius, 5) / 50; + scalingFactor *= 1 + smallCircleBonus; + } + + Distance = (t[0].StackedPosition - t[1].StackedPosition).Length * scalingFactor; + } + + private void setTimingValues() + { + // Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure. + DeltaTime = Math.Max(40, t[0].StartTime - t[1].StartTime); + TimeUntilHit = 450; // BaseObject.PreEmpt; + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs new file mode 100644 index 0000000000..aad53f6fe8 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Aim.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills +{ + /// + /// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances. + /// + public class Aim : Skill + { + protected override double SkillMultiplier => 26.25; + protected override double StrainDecayBase => 0.15; + + protected override double StrainValueOf(OsuDifficultyHitObject current) => Math.Pow(current.Distance, 0.99) / current.DeltaTime; + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs new file mode 100644 index 0000000000..b9632e18e2 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Skill.cs @@ -0,0 +1,100 @@ +// 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 osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; +using osu.Game.Rulesets.Osu.OsuDifficulty.Utils; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills +{ + /// + /// Used to processes strain values of s, keep track of strain levels caused by the processed objects + /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects. + /// + public abstract class Skill + { + /// + /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other. + /// + protected abstract double SkillMultiplier { get; } + + /// + /// Determines how quickly strain decays for the given skill. + /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second. + /// + protected abstract double StrainDecayBase { get; } + + /// + /// s that were processed previously. They can affect the strain values of the following objects. + /// + protected readonly History Previous = new History(2); // Contained objects not used yet + + private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap. + private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section. + private readonly List strainPeaks = new List(); + + /// + /// Process an and update current strain values accordingly. + /// + public void Process(OsuDifficultyHitObject current) + { + currentStrain *= strainDecay(current.DeltaTime); + if (!(current.BaseObject is Spinner)) + currentStrain += StrainValueOf(current) * SkillMultiplier; + + currentSectionPeak = Math.Max(currentStrain, currentSectionPeak); + + Previous.Push(current); + } + + /// + /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty. + /// + public void SaveCurrentPeak() + { + if (Previous.Count > 0) + strainPeaks.Add(currentSectionPeak); + } + + /// + /// Sets the initial strain level for a new section. + /// + /// The beginning of the new section in milliseconds + public void StartNewSectionFrom(double offset) + { + // The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries. + // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level. + if (Previous.Count > 0) + currentSectionPeak = currentStrain * strainDecay(offset - Previous[0].BaseObject.StartTime); + } + + /// + /// Returns the calculated difficulty value representing all processed s. + /// + public double DifficultyValue() + { + strainPeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain. + + double difficulty = 0; + double weight = 1; + + // Difficulty is the weighted sum of the highest strains from every section. + foreach (double strain in strainPeaks) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } + + /// + /// Calculates the strain value of an . This value is affected by previously processed objects. + /// + protected abstract double StrainValueOf(OsuDifficultyHitObject current); + + private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs new file mode 100644 index 0000000000..6c43c53e35 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Skills/Speed.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills +{ + /// + /// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit. + /// + public class Speed : Skill + { + protected override double SkillMultiplier => 1400; + protected override double StrainDecayBase => 0.3; + + private const double single_spacing_threshold = 125; + private const double stream_spacing_threshold = 110; + private const double almost_diameter = 90; + + protected override double StrainValueOf(OsuDifficultyHitObject current) + { + double distance = current.Distance; + + double speedValue; + if (distance > single_spacing_threshold) + speedValue = 2.5; + else if (distance > stream_spacing_threshold) + speedValue = 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold); + else if (distance > almost_diameter) + speedValue = 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter); + else if (distance > almost_diameter / 2) + speedValue = 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2); + else + speedValue = 0.95; + + return speedValue / current.DeltaTime; + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs new file mode 100644 index 0000000000..d2c2e1d774 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Utils/History.cs @@ -0,0 +1,86 @@ +// 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; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils +{ + /// + /// An indexed stack with Push() only, which disposes items at the bottom after the capacity is full. + /// Indexing starts at the top of the stack. + /// + public class History : IEnumerable + { + public int Count { get; private set; } + + private readonly T[] array; + private readonly int capacity; + private int marker; // Marks the position of the most recently added item. + + /// + /// Initializes a new instance of the History class that is empty and has the specified capacity. + /// + /// The number of items the History can hold. + public History(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException(); + + this.capacity = capacity; + array = new T[capacity]; + marker = capacity; // Set marker to the end of the array, outside of the indexed range by one. + } + + /// + /// The most recently added item is returned at index 0. + /// + public T this[int i] + { + get + { + if (i < 0 || i > Count - 1) + throw new IndexOutOfRangeException(); + + i += marker; + if (i > capacity - 1) + i -= capacity; + + return array[i]; + } + } + + /// + /// Adds the item as the most recent one in the history. + /// The oldest item is disposed if the history is full. + /// + public void Push(T item) // Overwrite the oldest item instead of shifting every item by one with every addition. + { + if (marker == 0) + marker = capacity - 1; + else + --marker; + + array[marker] = item; + + if (Count < capacity) + ++Count; + } + + /// + /// Returns an enumerator which enumerates items in the history starting from the most recently added one. + /// + public IEnumerator GetEnumerator() + { + for (int i = marker; i < capacity; ++i) + yield return array[i]; + + if (Count == capacity) + for (int i = 0; i < marker; ++i) + yield return array[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs deleted file mode 100644 index 5669993e67..0000000000 --- a/osu.Game.Rulesets.Osu/OsuDifficultyCalculator.cs +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Beatmaps; -using osu.Game.Rulesets.Osu.Beatmaps; -using osu.Game.Rulesets.Osu.Objects; -using System; -using System.Collections.Generic; - -namespace osu.Game.Rulesets.Osu -{ - public class OsuDifficultyCalculator : DifficultyCalculator - { - private const double star_scaling_factor = 0.0675; - private const double extreme_scaling_factor = 0.5; - - /// - /// HitObjects are stored as a member variable. - /// - internal List DifficultyHitObjects = new List(); - - public OsuDifficultyCalculator(Beatmap beatmap) : base(beatmap) - { - } - - protected override void PreprocessHitObjects() - { - foreach (var h in Objects) - (h as Slider)?.Curve?.Calculate(); - } - - protected override double CalculateInternal(Dictionary categoryDifficulty) - { - // Fill our custom DifficultyHitObject class, that carries additional information - DifficultyHitObjects.Clear(); - - foreach (var hitObject in Objects) - DifficultyHitObjects.Add(new OsuHitObjectDifficulty(hitObject)); - - // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. - DifficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); - - if (!CalculateStrainValues()) return 0; - - double speedDifficulty = CalculateDifficulty(DifficultyType.Speed); - double aimDifficulty = CalculateDifficulty(DifficultyType.Aim); - - // OverallDifficulty is not considered in this algorithm and neither is HpDrainRate. That means, that in this form the algorithm determines how hard it physically is - // to play the map, assuming, that too much of an error will not lead to a death. - // It might be desirable to include OverallDifficulty into map difficulty, but in my personal opinion it belongs more to the weighting of the actual peformance - // and is superfluous in the beatmap difficulty rating. - // If it were to be considered, then I would look at the hit window of normal HitCircles only, since Sliders and Spinners are (almost) "free" 300s and take map length - // into account as well. - - // The difficulty can be scaled by any desired metric. - // In osu!tp it gets squared to account for the rapid increase in difficulty as the limit of a human is approached. (Of course it also gets scaled afterwards.) - // It would not be suitable for a star rating, therefore: - - // The following is a proposal to forge a star rating from 0 to 5. It consists of taking the square root of the difficulty, since by simply scaling the easier - // 5-star maps would end up with one star. - double speedStars = Math.Sqrt(speedDifficulty) * star_scaling_factor; - double aimStars = Math.Sqrt(aimDifficulty) * star_scaling_factor; - - if (categoryDifficulty != null) - { - categoryDifficulty.Add("Aim", aimStars.ToString("0.00")); - categoryDifficulty.Add("Speed", speedStars.ToString("0.00")); - - double hitWindow300 = 30/*HitObjectManager.HitWindow300*/ / TimeRate; - double preEmpt = 450/*HitObjectManager.PreEmpt*/ / TimeRate; - - categoryDifficulty.Add("OD", (-(hitWindow300 - 80.0) / 6.0).ToString("0.00")); - categoryDifficulty.Add("AR", (preEmpt > 1200.0 ? -(preEmpt - 1800.0) / 120.0 : -(preEmpt - 1200.0) / 150.0 + 5.0).ToString("0.00")); - - int maxCombo = 0; - foreach (OsuHitObjectDifficulty hitObject in DifficultyHitObjects) - maxCombo += hitObject.MaxCombo; - - categoryDifficulty.Add("Max combo", maxCombo.ToString()); - } - - // Again, from own observations and from the general opinion of the community a map with high speed and low aim (or vice versa) difficulty is harder, - // than a map with mediocre difficulty in both. Therefore we can not just add both difficulties together, but will introduce a scaling that favors extremes. - double starRating = speedStars + aimStars + Math.Abs(speedStars - aimStars) * extreme_scaling_factor; - // Another approach to this would be taking Speed and Aim separately to a chosen power, which again would be equivalent. This would be more convenient if - // the hit window size is to be considered as well. - - // Note: The star rating is tuned extremely tight! Airman (/b/104229) and Freedom Dive (/b/126645), two of the hardest ranked maps, both score ~4.66 stars. - // Expect the easier kind of maps that officially get 5 stars to obtain around 2 by this metric. The tutorial still scores about half a star. - // Tune by yourself as you please. ;) - - return starRating; - } - - protected bool CalculateStrainValues() - { - // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. - using (List.Enumerator hitObjectsEnumerator = DifficultyHitObjects.GetEnumerator()) - { - - if (!hitObjectsEnumerator.MoveNext()) return false; - - OsuHitObjectDifficulty current = hitObjectsEnumerator.Current; - - // First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject. - while (hitObjectsEnumerator.MoveNext()) - { - var next = hitObjectsEnumerator.Current; - next?.CalculateStrains(current, TimeRate); - current = next; - } - - return true; - } - } - - /// - /// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP. - /// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain. - /// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage. - /// - protected const double STRAIN_STEP = 400; - - /// - /// The weighting of each strain value decays to this number * it's previous value - /// - protected const double DECAY_WEIGHT = 0.9; - - protected double CalculateDifficulty(DifficultyType type) - { - double actualStrainStep = STRAIN_STEP * TimeRate; - - // Find the highest strain value within each strain step - List highestStrains = new List(); - double intervalEndTime = actualStrainStep; - double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval - - OsuHitObjectDifficulty previousHitObject = null; - foreach (OsuHitObjectDifficulty hitObject in DifficultyHitObjects) - { - // While we are beyond the current interval push the currently available maximum to our strain list - while (hitObject.BaseHitObject.StartTime > intervalEndTime) - { - highestStrains.Add(maximumStrain); - - // The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay - // until the beginning of the next interval. - if (previousHitObject == null) - { - maximumStrain = 0; - } - else - { - double decay = Math.Pow(OsuHitObjectDifficulty.DECAY_BASE[(int)type], (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); - maximumStrain = previousHitObject.Strains[(int)type] * decay; - } - - // Go to the next time interval - intervalEndTime += actualStrainStep; - } - - // Obtain maximum strain - maximumStrain = Math.Max(hitObject.Strains[(int)type], maximumStrain); - - previousHitObject = hitObject; - } - - // Build the weighted sum over the highest strains for each interval - double difficulty = 0; - double weight = 1; - highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain. - - foreach (double strain in highestStrains) - { - difficulty += weight * strain; - weight *= DECAY_WEIGHT; - } - - return difficulty; - } - - protected override BeatmapConverter CreateBeatmapConverter() => new OsuBeatmapConverter(); - - // Those values are used as array indices. Be careful when changing them! - public enum DifficultyType - { - Speed = 0, - Aim, - }; - } -} diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs new file mode 100644 index 0000000000..a65d28cec0 --- /dev/null +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu +{ + public class OsuInputManager : RulesetInputManager + { + public OsuInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + } + + public enum OsuAction + { + [Description("Left Button")] + LeftButton, + [Description("Right Button")] + RightButton + } +} diff --git a/osu.Game.Rulesets.Osu/OsuKeyConversionInputManager.cs b/osu.Game.Rulesets.Osu/OsuKeyConversionInputManager.cs deleted file mode 100644 index d60aab90fb..0000000000 --- a/osu.Game.Rulesets.Osu/OsuKeyConversionInputManager.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Linq; -using osu.Framework.Input; -using osu.Game.Screens.Play; -using OpenTK.Input; -using KeyboardState = osu.Framework.Input.KeyboardState; -using MouseState = osu.Framework.Input.MouseState; - -namespace osu.Game.Rulesets.Osu -{ - public class OsuKeyConversionInputManager : KeyConversionInputManager - { - private bool leftViaKeyboard; - private bool rightViaKeyboard; - - protected override void TransformState(InputState state) - { - base.TransformState(state); - - var mouse = state.Mouse as MouseState; - var keyboard = state.Keyboard as KeyboardState; - - if (keyboard != null) - { - leftViaKeyboard = keyboard.Keys.Contains(Key.Z); - rightViaKeyboard = keyboard.Keys.Contains(Key.X); - } - - if (mouse != null) - { - if (leftViaKeyboard) - mouse.SetPressed(MouseButton.Left, true); - if (rightViaKeyboard) - mouse.SetPressed(MouseButton.Right, true); - } - } - } -} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index af4a099e0d..00413331e0 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -1,28 +1,38 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.OsuDifficulty; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using System.Collections.Generic; using System.Linq; +using osu.Framework.Graphics; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; +using osu.Game.Overlays.Settings; +using osu.Framework.Input.Bindings; namespace osu.Game.Rulesets.Osu { public class OsuRuleset : Ruleset { - public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new OsuHitRenderer(beatmap, isForCurrentRuleset); + public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new OsuRulesetContainer(this, beatmap, isForCurrentRuleset); + + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] + { + new KeyBinding(InputKey.Z, OsuAction.LeftButton), + new KeyBinding(InputKey.X, OsuAction.RightButton), + new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton), + new KeyBinding(InputKey.MouseRight, OsuAction.RightButton), + }; public override IEnumerable GetBeatmapStatistics(WorkingBeatmap beatmap) => new[] - { + { new BeatmapStatistic { Name = @"Circle count", @@ -46,7 +56,14 @@ namespace osu.Game.Rulesets.Osu { new OsuModEasy(), new OsuModNoFail(), - new OsuModHalfTime(), + new MultiMod + { + Mods = new Mod[] + { + new OsuModHalfTime(), + new OsuModDaycore(), + }, + }, }; case ModType.DifficultyIncrease: @@ -95,22 +112,23 @@ namespace osu.Game.Rulesets.Osu } } - public override FontAwesome Icon => FontAwesome.fa_osu_osu_o; + public override Mod GetAutoplayMod() => new OsuModAutoplay(); + + public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o }; public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new OsuDifficultyCalculator(beatmap); public override string Description => "osu!"; - public override IEnumerable CreateGameplayKeys() => new KeyCounter[] - { - new KeyCounterKeyboard(Key.Z), - new KeyCounterKeyboard(Key.X), - new KeyCounterMouse(MouseButton.Left), - new KeyCounterMouse(MouseButton.Right) - }; - public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + public override SettingsSubsection CreateSettings() => new OsuSettings(); + public override int LegacyID => 0; + + public OsuRuleset(RulesetInfo rulesetInfo) + : base(rulesetInfo) + { + } } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 5ede3f56f5..b92f1bc60a 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Replays /// /// What easing to use when moving between hitobjects /// - private EasingTypes preferredEasing => DelayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out; + private Easing preferredEasing => DelayedMovements ? Easing.InOutCubic : Easing.Out; #endregion @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Replays { // Default values for circles/sliders Vector2 startPosition = h.StackedPosition; - EasingTypes easing = preferredEasing; + Easing 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 @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Replays 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; + easing = Easing.In; } } @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Replays } } - private void moveToHitObject(double targetTime, Vector2 targetPos, double hitObjectRadius, EasingTypes easing) + private void moveToHitObject(double targetTime, Vector2 targetPos, double hitObjectRadius, Easing easing) { ReplayFrame lastFrame = Frames[Frames.Count - 1]; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 079ee928af..856ca0c98d 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Osu.Scoring { } - public OsuScoreProcessor(HitRenderer hitRenderer) - : base(hitRenderer) + public OsuScoreProcessor(RulesetContainer rulesetContainer) + : base(rulesetContainer) { } diff --git a/osu.Game/Graphics/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs similarity index 94% rename from osu.Game/Graphics/Cursor/CursorTrail.cs rename to osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 11475a0f19..a5be6a7952 100644 --- a/osu.Game/Graphics/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -1,22 +1,22 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.OpenGL.Buffers; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Textures; using osu.Framework.Input; -using OpenTK; -using System; -using osu.Framework.Graphics.OpenGL; -using osu.Framework.Graphics.OpenGL.Buffers; -using OpenTK.Graphics.ES30; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Colour; using osu.Framework.Timing; -using System.Diagnostics; +using OpenTK; +using OpenTK.Graphics.ES30; -namespace osu.Game.Graphics.Cursor +namespace osu.Game.Rulesets.Osu.UI.Cursor { internal class CursorTrail : Drawable { @@ -65,7 +65,6 @@ namespace osu.Game.Graphics.Cursor // as we are currently very dependent on having a running clock, let's make our own clock for the time being. Clock = new FramedClock(); - AlwaysReceiveInput = true; RelativeSizeAxes = Axes.Both; for (int i = 0; i < max_sprites; i++) @@ -75,6 +74,8 @@ namespace osu.Game.Graphics.Cursor } } + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; + [BackgroundDependencyLoader] private void load(ShaderManager shaders, TextureStore textures) { diff --git a/osu.Game/Graphics/Cursor/GameplayCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs similarity index 81% rename from osu.Game/Graphics/Cursor/GameplayCursor.cs rename to osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs index 801fe1d011..adfc946f86 100644 --- a/osu.Game/Graphics/Cursor/GameplayCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs @@ -1,23 +1,22 @@ // 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 osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Database; +using OpenTK; +using OpenTK.Graphics; -namespace osu.Game.Graphics.Cursor +namespace osu.Game.Rulesets.Osu.UI.Cursor { - public class GameplayCursor : CursorContainer + public class GameplayCursor : CursorContainer, IKeyBindingHandler { protected override Drawable CreateCursor() => new OsuCursor(); @@ -26,19 +25,7 @@ namespace osu.Game.Graphics.Cursor Add(new CursorTrail { Depth = 1 }); } - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - ActiveCursor.Scale = new Vector2(1); - ActiveCursor.ScaleTo(1.2f, 100, EasingTypes.OutQuad); - return base.OnMouseDown(state, args); - } - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) - { - if (!state.Mouse.HasMainButtonPressed) - ActiveCursor.ScaleTo(1, 200, EasingTypes.OutQuad); - return base.OnMouseUp(state, args); - } + private int downCount; public class OsuCursor : Container { @@ -67,7 +54,7 @@ namespace osu.Game.Graphics.Cursor Masking = true, BorderThickness = Size.X / 6, BorderColour = Color4.White, - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Colour = Color4.Pink.Opacity(0.5f), @@ -144,5 +131,33 @@ namespace osu.Game.Graphics.Cursor cursorContainer.Scale = new Vector2(scale); } } + + public bool OnPressed(OsuAction action) + { + switch (action) + { + case OsuAction.LeftButton: + case OsuAction.RightButton: + downCount++; + ActiveCursor.ScaleTo(1).ScaleTo(1.2f, 100, Easing.OutQuad); + break; + } + + return false; + } + + public bool OnReleased(OsuAction action) + { + switch (action) + { + case OsuAction.LeftButton: + case OsuAction.RightButton: + if (--downCount == 0) + ActiveCursor.ScaleTo(1, 200, Easing.OutQuad); + break; + } + + return false; + } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 53eedea073..9b88c9d1b3 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -10,8 +10,8 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.UI; using System.Linq; -using osu.Game.Graphics.Cursor; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.UI.Cursor; namespace osu.Game.Rulesets.Osu.UI { @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.UI Anchor = Anchor.Centre; Origin = Anchor.Centre; - Add(new Drawable[] + AddRange(new Drawable[] { connectionLayer = new FollowPointRenderer { @@ -80,9 +80,9 @@ namespace osu.Game.Rulesets.Osu.UI public override void PostProcess() { - connectionLayer.HitObjects = HitObjects.Children + connectionLayer.HitObjects = HitObjects.Objects .Select(d => d.HitObject) - .OrderBy(h => h.StartTime); + .OrderBy(h => h.StartTime).OfType(); } public override void OnJudgement(DrawableHitObject judgedObject) diff --git a/osu.Game.Rulesets.Osu/UI/OsuHitRenderer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs similarity index 79% rename from osu.Game.Rulesets.Osu/UI/OsuHitRenderer.cs rename to osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index e582d2fcd3..083f11945c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuHitRenderer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -1,6 +1,7 @@ // 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 OpenTK; using osu.Game.Beatmaps; using osu.Game.Rulesets.Beatmaps; @@ -12,14 +13,13 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Osu.UI { - public class OsuHitRenderer : HitRenderer + public class OsuRulesetContainer : RulesetContainer { - public OsuHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(beatmap, isForCurrentRuleset) + public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(ruleset, beatmap, isForCurrentRuleset) { } @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override Playfield CreatePlayfield() => new OsuPlayfield(); - protected override KeyConversionInputManager CreateKeyConversionInputManager() => new OsuKeyConversionInputManager(); + public override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo); protected override DrawableHitObject GetVisualRepresentation(OsuHitObject h) { diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuSettings.cs new file mode 100644 index 0000000000..b820f33b39 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OsuSettings.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Rulesets.Osu.UI +{ + public class OsuSettings : SettingsSubsection + { + protected override string Header => "osu!"; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = "Snaking in sliders", + Bindable = config.GetBindable(OsuSetting.SnakingInSliders) + }, + new SettingsCheckbox + { + LabelText = "Snaking out sliders", + Bindable = config.GetBindable(OsuSetting.SnakingOutSliders) + }, + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index b91bdc6a78..1422ded407 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -34,8 +34,9 @@ false - - $(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll + + $(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll + True @@ -68,12 +69,20 @@ - - - + + + + + + + + + + + - + diff --git a/osu.Game.Rulesets.Osu/packages.config b/osu.Game.Rulesets.Osu/packages.config index 634d0b51f6..fa6edb9c8f 100644 --- a/osu.Game.Rulesets.Osu/packages.config +++ b/osu.Game.Rulesets.Osu/packages.config @@ -5,5 +5,5 @@ Copyright (c) 2007-2017 ppy Pty Ltd . Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE --> - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 5d44da78f9..2bf058fc2b 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Taiko.Objects; using System; using System.Collections.Generic; using System.Linq; -using osu.Game.Database; using osu.Game.IO.Serialization; using osu.Game.Audio; using osu.Game.Rulesets.Beatmaps; @@ -40,15 +39,22 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps /// private const float taiko_base_distance = 100; + private readonly bool isForCurrentRuleset; + protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(HitObject) }; - protected override Beatmap ConvertBeatmap(Beatmap original, bool isForCurrentRuleset) + public TaikoBeatmapConverter(bool isForCurrentRuleset) + { + this.isForCurrentRuleset = isForCurrentRuleset; + } + + protected override Beatmap ConvertBeatmap(Beatmap original) { // Rewrite the beatmap info to add the slider velocity multiplier BeatmapInfo info = original.BeatmapInfo.DeepClone(); info.Difficulty.SliderMultiplier *= legacy_velocity_multiplier; - Beatmap converted = base.ConvertBeatmap(original, isForCurrentRuleset); + Beatmap converted = base.ConvertBeatmap(original); // Post processing step to transform hit objects with the same start time into strong hits converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => @@ -82,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime); double speedAdjustment = difficultyPoint.SpeedMultiplier; - double speedAdjustedBeatLength = timingPoint.BeatLength * speedAdjustment; + double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment; // The true distance, accounting for any repeats. This ends up being the drum roll distance later double distance = distanceData.Distance * repeats * legacy_velocity_multiplier; @@ -95,7 +101,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // For some reason, old osu! always uses speedAdjustment to determine the taiko velocity, but // only uses it to determine osu! velocity if beatmap version < 8. Let's account for that here. if (beatmap.BeatmapInfo.BeatmapVersion >= 8) - speedAdjustedBeatLength /= speedAdjustment; + speedAdjustedBeatLength *= speedAdjustment; // The velocity of the osu! hit object - calculated as the velocity of a slider double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; @@ -105,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.Difficulty.SliderTickRate, taikoDuration / repeats); - if (tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) + if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) { List allSamples = curveData != null ? curveData.RepeatSamples : new List(new[] { samples }); diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoMod.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoMod.cs index 8b7a099b9a..abaa8c1bc1 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoMod.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoMod.cs @@ -37,6 +37,11 @@ namespace osu.Game.Rulesets.Taiko.Mods } + public class TaikoModDaycore : ModDaycore + { + public override double ScoreMultiplier => 0.5; + } + public class TaikoModDoubleTime : ModDoubleTime { public override double ScoreMultiplier => 1.12; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index 4c83e08bab..9df1b41a8b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -2,16 +2,17 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using OpenTK; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { /// /// A line that scrolls alongside hit objects in the playfield and visualises control points. /// - public class DrawableBarLine : Container + public class DrawableBarLine : DrawableScrollingHitObject { /// /// The width of the line tracker. @@ -34,15 +35,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected readonly BarLine BarLine; public DrawableBarLine(BarLine barLine) + : base(barLine) { BarLine = barLine; Anchor = Anchor.CentreLeft; Origin = Anchor.Centre; - RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Y; - Width = tracker_width; Children = new[] @@ -56,25 +56,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Alpha = 0.75f } }; - - LifetimeStart = BarLine.StartTime - BarLine.ScrollTime * 2; - LifetimeEnd = BarLine.StartTime + BarLine.ScrollTime; } - protected override void LoadComplete() + protected override TaikoJudgement CreateJudgement() => null; + + protected override void UpdateState(ArmedState state) { - base.LoadComplete(); - Delay(BarLine.StartTime - Time.Current); - FadeOut(base_fadeout_time * BarLine.ScrollTime / 1000); - } - - private void updateScrollPosition(double time) => MoveToX((float)((BarLine.StartTime - time) / BarLine.ScrollTime)); - - protected override void Update() - { - base.Update(); - - updateScrollPosition(Time.Current); } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs index e64682a1e4..7a4cf1f1f7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs @@ -3,8 +3,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using OpenTK; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -20,10 +20,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private const float triangle_size = 20f; + private readonly Container triangleContainer; + public DrawableBarLineMajor(BarLine barLine) : base(barLine) { - Add(new Container + Add(triangleContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -53,5 +55,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Tracker.Alpha = 1f; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + using (triangleContainer.BeginAbsoluteSequence(HitObject.StartTime)) + triangleContainer.FadeOut(150); + } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 8bb78669ca..26aa20f1af 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -4,13 +4,12 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableCentreHit : DrawableHit { - protected override Key[] HitKeys { get; } = { Key.F, Key.J }; + protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; public DrawableCentreHit(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs index 434fb9377f..1dbab408de 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs @@ -4,13 +4,12 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableCentreHitStrong : DrawableHitStrong { - protected override Key[] HitKeys { get; } = { Key.F, Key.J }; + protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; public DrawableCentreHitStrong(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 4562501ed1..5d6ea8f51e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -10,6 +10,8 @@ using osu.Game.Rulesets.Taiko.Judgements; using OpenTK; using OpenTK.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -30,27 +32,31 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public DrawableDrumRoll(DrumRoll drumRoll) : base(drumRoll) { + Width = (float)HitObject.Duration; + + Container tickContainer; + MainPiece.Add(tickContainer = new Container + { + RelativeSizeAxes = Axes.Both, + RelativeChildOffset = new Vector2((float)HitObject.StartTime, 0), + RelativeChildSize = new Vector2((float)HitObject.Duration, 1) + }); + foreach (var tick in drumRoll.Ticks) { - var newTick = new DrawableDrumRollTick(tick) - { - X = (float)((tick.StartTime - HitObject.StartTime) / HitObject.Duration) - }; - + var newTick = new DrawableDrumRollTick(tick); newTick.OnJudgement += onTickJudgement; AddNested(newTick); - MainPiece.Add(newTick); + tickContainer.Add(newTick); } } protected override TaikoJudgement CreateJudgement() => new TaikoJudgement { SecondHit = HitObject.IsStrong }; - protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece(HitObject.IsStrong) - { - Length = (float)(HitObject.Duration / HitObject.ScrollTime), - PlayfieldLengthReference = () => Parent.DrawSize.X - }; + protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece(); + + public override bool OnPressed(TaikoAction action) => false; [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -59,17 +65,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables accentDarkColour = colours.YellowDarker; } - protected override void LoadComplete() - { - base.LoadComplete(); - - // This is naive, however it's based on the reasoning that the hit target - // is further than mid point of the play field, so the time taken to scroll in should always - // be greater than the time taken to scroll out to the left of the screen. - // Thus, using PreEmpt here is enough for the drum roll to completely scroll out. - LifetimeEnd = HitObject.EndTime + HitObject.ScrollTime; - } - private void onTickJudgement(DrawableHitObject obj) { if (obj.Judgement.Result == HitResult.Hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 56a747467e..e94886df16 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; -using OpenTK.Input; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -15,6 +14,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public DrawableDrumRollTick(DrumRollTick tick) : base(tick) { + // Because ticks aren't added by the ScrollingPlayfield, we need to set the following properties ourselves + RelativePositionAxes = Axes.X; + X = (float)tick.StartTime; + + FillMode = FillMode.Fit; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // We need to set this here because RelativeSizeAxes won't/can't set our size by default with a different RelativeChildSize + Width *= Parent.RelativeChildSize.X; } protected override TaikoPiece CreateMainPiece() => new TickPiece @@ -41,17 +53,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (state) { case ArmedState.Hit: - Content.ScaleTo(0, 100, EasingTypes.OutQuint); + Content.ScaleTo(0, 100, Easing.OutQuint); break; } } - protected override void UpdateScrollPosition(double time) - { - // Ticks don't move - } - - protected override bool HandleKeyPress(Key key) + public override bool OnPressed(TaikoAction action) { return Judgement.Result == HitResult.None && UpdateJudgement(true); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index a4a46e3b48..5fcae7d0f4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// A list of keys which can result in hits for this HitObject. /// - protected abstract Key[] HitKeys { get; } + protected abstract TaikoAction[] HitActions { get; } /// /// Whether the last key pressed is a valid hit key. @@ -26,6 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected DrawableHit(Hit hit) : base(hit) { + FillMode = FillMode.Fit; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // We need to set this here because RelativeSizeAxes won't/can't set our size by default with a different RelativeChildSize + Width *= Parent.RelativeChildSize.X; } protected override void CheckJudgement(bool userTriggered) @@ -53,57 +61,55 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Judgement.Result = HitResult.Miss; } - protected override bool HandleKeyPress(Key key) + public override bool OnPressed(TaikoAction action) { if (Judgement.Result != HitResult.None) return false; - validKeyPressed = HitKeys.Contains(key); + validKeyPressed = HitActions.Contains(action); return UpdateJudgement(true); } protected override void UpdateState(ArmedState state) { - Delay(HitObject.StartTime - Time.Current + Judgement.TimeOffset, true); - var circlePiece = MainPiece as CirclePiece; + circlePiece?.FlashBox.FinishTransforms(); - circlePiece?.FlashBox.Flush(); - - switch (State) + using (BeginDelayedSequence(HitObject.StartTime - Time.Current + Judgement.TimeOffset, true)) { - case ArmedState.Idle: - Delay(HitObject.HitWindowMiss); - break; - case ArmedState.Miss: - FadeOut(100); - break; - case ArmedState.Hit: - FadeOut(600); + switch (State) + { + case ArmedState.Idle: + this.Delay(HitObject.HitWindowMiss).Expire(); + break; + case ArmedState.Miss: + this.FadeOut(100) + .Expire(); + break; + case ArmedState.Hit: + var flash = circlePiece?.FlashBox; + if (flash != null) + { + flash.FadeTo(0.9f); + flash.FadeOut(300); + } - var flash = circlePiece?.FlashBox; - if (flash != null) - { - flash.FadeTo(0.9f); - flash.FadeOut(300); - } + const float gravity_time = 300; + const float gravity_travel_height = 200; + Content.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad); - FadeOut(800); + this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out) + .Then() + .MoveToY(gravity_travel_height * 2, gravity_time * 2, Easing.In); - const float gravity_time = 300; - const float gravity_travel_height = 200; + this.FadeOut(800) + .Expire(); - Content.ScaleTo(0.8f, gravity_time * 2, EasingTypes.OutQuad); - - MoveToY(-gravity_travel_height, gravity_time, EasingTypes.Out); - Delay(gravity_time, true); - MoveToY(gravity_travel_height * 2, gravity_time * 2, EasingTypes.In); - break; + break; + } } - - Expire(); } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs index 1c6b12ea43..256c50d33a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs @@ -3,11 +3,8 @@ using System; using System.Linq; -using osu.Framework.Input; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; -using OpenTK.Input; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -21,15 +18,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private double firstHitTime; private bool firstKeyHeld; - private Key firstHitKey; + private TaikoAction firstHitAction; protected DrawableHitStrong(Hit hit) : base(hit) { } - protected override TaikoPiece CreateMainPiece() => new CirclePiece(true); - protected override TaikoJudgement CreateJudgement() => new TaikoStrongHitJudgement(); protected override void CheckJudgement(bool userTriggered) @@ -49,18 +44,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Judgement.SecondHit = true; } - protected override bool HandleKeyPress(Key key) + public override bool OnReleased(TaikoAction action) + { + if (action == firstHitAction) + firstKeyHeld = false; + return base.OnReleased(action); + } + + public override bool OnPressed(TaikoAction action) { // Check if we've handled the first key if (Judgement.Result == HitResult.None) { // First key hasn't been handled yet, attempt to handle it - bool handled = base.HandleKeyPress(key); + bool handled = base.OnPressed(action); if (handled) { firstHitTime = Time.Current; - firstHitKey = key; + firstHitAction = action; + firstKeyHeld = true; } return handled; @@ -71,22 +74,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return false; // Don't handle represses of the first key - if (firstHitKey == key) + if (firstHitAction == action) return false; - // Don't handle invalid hit key presses - if (!HitKeys.Contains(key)) + // Don't handle invalid hit action presses + if (!HitActions.Contains(action)) return false; // Assume the intention was to hit the strong hit with both keys only if the first key is still being held down return firstKeyHeld && UpdateJudgement(true); } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - firstKeyHeld = state.Keyboard.Keys.Contains(firstHitKey); - - return base.OnKeyDown(state, args); - } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 20e8d36105..9f0cd3e061 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -4,13 +4,12 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableRimHit : DrawableHit { - protected override Key[] HitKeys { get; } = { Key.D, Key.K }; + protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; public DrawableRimHit(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs index 4b1bb62bab..294198f3ee 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs @@ -4,13 +4,12 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableRimHitStrong : DrawableHitStrong { - protected override Key[] HitKeys { get; } = { Key.D, Key.K }; + protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; public DrawableRimHitStrong(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 37efd8aba4..fc3bc82520 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -7,14 +7,13 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using OpenTK; using OpenTK.Graphics; -using OpenTK.Input; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -35,11 +34,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; - private readonly CirclePiece circlePiece; - - private readonly Key[] rimKeys = { Key.D, Key.K }; - private readonly Key[] centreKeys = { Key.F, Key.J }; - private Key[] lastKeySet; + private readonly TaikoAction[] rimActions = { TaikoAction.LeftRim, TaikoAction.RightRim }; + private readonly TaikoAction[] centreActions = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; + private TaikoAction[] lastAction; /// /// The amount of times the user has hit this swell. @@ -52,93 +49,92 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public DrawableSwell(Swell swell) : base(swell) { - Children = new Drawable[] + FillMode = FillMode.Fit; + + Add(bodyContainer = new Container { - bodyContainer = new Container + RelativeSizeAxes = Axes.Both, + Depth = 1, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + expandingRing = new CircularContainer { - expandingRing = new CircularContainer + Name = "Expanding ring", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + BlendingMode = BlendingMode.Additive, + Masking = true, + Children = new[] { - Name = "Expanding ring", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Alpha = 0, - Size = new Vector2(TaikoHitObject.DEFAULT_CIRCLE_DIAMETER), - BlendingMode = BlendingMode.Additive, - Masking = true, - Children = new [] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = inner_ring_alpha, - } + RelativeSizeAxes = Axes.Both, + Alpha = inner_ring_alpha, } - }, - targetRing = new CircularContainer + } + }, + targetRing = new CircularContainer + { + Name = "Target ring (thick border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thick_border, + BlendingMode = BlendingMode.Additive, + Children = new Drawable[] { - Name = "Target ring (thick border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(TaikoHitObject.DEFAULT_CIRCLE_DIAMETER), - Masking = true, - BorderThickness = target_ring_thick_border, - BlendingMode = BlendingMode.Additive, - Children = new Drawable[] + new Box { - new Box + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new CircularContainer + { + Name = "Target ring (thin border)", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = target_ring_thin_border, + BorderColour = Color4.White, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - new CircularContainer - { - Name = "Target ring (thin border)", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = target_ring_thin_border, - BorderColour = Color4.White, - Children = new[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - } + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true } } } - }, - circlePiece = new CirclePiece - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new [] - { - symbol = new SwellSymbolPiece() - } } } } - }; + }); - circlePiece.KiaiMode = HitObject.Kiai; + MainPiece.Add(symbol = new SwellSymbolPiece()); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - circlePiece.AccentColour = colours.YellowDark; + MainPiece.AccentColour = colours.YellowDark; expandingRing.Colour = colours.YellowLight; targetRing.BorderColour = colours.YellowDark.Opacity(0.25f); } + protected override void LoadComplete() + { + base.LoadComplete(); + + // We need to set this here because RelativeSizeAxes won't/can't set our size by default with a different RelativeChildSize + Width *= Parent.RelativeChildSize.X; + } + protected override void CheckJudgement(bool userTriggered) { if (userTriggered) @@ -147,13 +143,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables var completion = (float)userHits / HitObject.RequiredHits; - expandingRing.FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50); - using (expandingRing.BeginDelayedSequence(50)) - expandingRing.FadeTo(completion / 8, 2000, EasingTypes.OutQuint); + expandingRing + .FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50) + .Then() + .FadeTo(completion / 8, 2000, Easing.OutQuint); - symbol.RotateTo((float)(completion * HitObject.Duration / 8), 4000, EasingTypes.OutQuint); + symbol.RotateTo((float)(completion * HitObject.Duration / 8), 4000, Easing.OutQuint); - expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, EasingTypes.OutQuint); + expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); if (userHits == HitObject.RequiredHits) { @@ -180,44 +177,40 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void UpdateState(ArmedState state) { const float preempt = 100; - - Delay(HitObject.StartTime - Time.Current - preempt, true); - - targetRing.ScaleTo(target_ring_scale, preempt * 4, EasingTypes.OutQuint); - - Delay(preempt, true); - - Delay(Judgement.TimeOffset + HitObject.Duration, true); - const float out_transition_time = 300; + double untilStartTime = HitObject.StartTime - Time.Current; + double untilJudgement = untilStartTime + Judgement.TimeOffset + HitObject.Duration; + + targetRing.Delay(untilStartTime - preempt).ScaleTo(target_ring_scale, preempt * 4, Easing.OutQuint); + this.Delay(untilJudgement).FadeOut(out_transition_time, Easing.Out); + switch (state) { case ArmedState.Hit: - bodyContainer.ScaleTo(1.4f, out_transition_time); + bodyContainer.Delay(untilJudgement).ScaleTo(1.4f, out_transition_time); break; } - FadeOut(out_transition_time, EasingTypes.Out); - Expire(); } - protected override void UpdateScrollPosition(double time) + protected override void Update() { - // Make the swell stop at the hit target - double t = Math.Min(HitObject.StartTime, time); + base.Update(); + // Make the swell stop at the hit target + X = (float)Math.Max(Time.Current, HitObject.StartTime); + + double t = Math.Min(HitObject.StartTime, Time.Current); if (t == HitObject.StartTime && !hasStarted) { OnStart?.Invoke(); hasStarted = true; } - - base.UpdateScrollPosition(t); } - protected override bool HandleKeyPress(Key key) + public override bool OnPressed(TaikoAction action) { if (Judgement.Result != HitResult.None) return false; @@ -227,12 +220,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return false; // Find the keyset which this key corresponds to - var keySet = rimKeys.Contains(key) ? rimKeys : centreKeys; + var keySet = rimActions.Contains(action) ? rimActions : centreActions; // Ensure alternating keysets - if (keySet == lastKeySet) + if (keySet == lastAction) return false; - lastKeySet = keySet; + lastAction = keySet; UpdateJudgement(true); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 24aa366944..765002fb34 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -1,35 +1,23 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using OpenTK; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public abstract class DrawableTaikoHitObject : DrawableHitObject - where TaikoHitType : TaikoHitObject + public abstract class DrawableTaikoHitObject + : DrawableScrollingHitObject, IKeyBindingHandler + where TaikoHitType : TaikoHitObject { - /// - /// A list of keys which this hit object will accept. These are the standard Taiko keys for now. - /// These should be moved to bindings later. - /// - private readonly List validKeys = new List(new[] { Key.D, Key.F, Key.J, Key.K }); - public override Vector2 OriginPosition => new Vector2(DrawHeight / 2); - protected override Container Content => bodyContainer; - protected readonly TaikoPiece MainPiece; - private readonly Container bodyContainer; - public new TaikoHitType HitObject; protected DrawableTaikoHitObject(TaikoHitType hitObject) @@ -40,54 +28,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Anchor = Anchor.CentreLeft; Origin = Anchor.Custom; - AutoSizeAxes = Axes.Both; - - RelativePositionAxes = Axes.X; - - AddInternal(bodyContainer = new Container - { - AutoSizeAxes = Axes.Both, - Children = new[] - { - MainPiece = CreateMainPiece() - } - }); + RelativeSizeAxes = Axes.Both; + Size = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); + Add(MainPiece = CreateMainPiece()); MainPiece.KiaiMode = HitObject.Kiai; - - LifetimeStart = HitObject.StartTime - HitObject.ScrollTime * 2; } protected override TaikoJudgement CreateJudgement() => new TaikoJudgement(); - protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(HitObject.IsStrong); + protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(); - /// - /// Sets the scroll position of the DrawableHitObject relative to the offset between - /// a time value and the HitObject's StartTime. - /// - /// - protected virtual void UpdateScrollPosition(double time) => X = (float)((HitObject.StartTime - time) / HitObject.ScrollTime); + public abstract bool OnPressed(TaikoAction action); - protected override void Update() - { - UpdateScrollPosition(Time.Current); - } - - protected virtual bool HandleKeyPress(Key key) => false; - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - // Make sure we don't handle held-down keys - if (args.Repeat) - return false; - - // Check if we've pressed a valid taiko key - if (!validKeys.Contains(args.Key)) - return false; - - // Handle it! - return HandleKeyPress(args.Key); - } + public virtual bool OnReleased(TaikoAction action) => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs index ddf1492ecc..f4c78251d0 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitSymbolPiece.cs @@ -3,27 +3,32 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using OpenTK; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { /// /// The symbol used for centre hit pieces. /// - public class CentreHitSymbolPiece : CircularContainer + public class CentreHitSymbolPiece : Container { public CentreHitSymbolPiece() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(CirclePiece.SYMBOL_INNER_SIZE); - Masking = true; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(CirclePiece.SYMBOL_SIZE); + Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER); + Children = new[] { - new Box + new CircularContainer { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new[] { new Box { RelativeSizeAxes = Axes.Both } } } }; } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs index 3ea05b6558..ba717371dd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Backgrounds; using OpenTK.Graphics; using osu.Game.Beatmaps.ControlPoints; @@ -21,9 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces /// public class CirclePiece : TaikoPiece { - public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_CIRCLE_DIAMETER * 0.45f; + public const float SYMBOL_SIZE = 0.45f; public const float SYMBOL_BORDER = 8; - public const float SYMBOL_INNER_SIZE = SYMBOL_SIZE - 2 * SYMBOL_BORDER; private const double pre_beat_transition_time = 80; /// @@ -64,11 +63,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public Box FlashBox; - public CirclePiece(bool isStrong = false) + public CirclePiece() { EarlyActivationMilliseconds = pre_beat_transition_time; - AddInternal(new Drawable[] + AddRangeInternal(new Drawable[] { background = new CircularContainer { @@ -120,35 +119,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces }, content = new Container { - RelativeSizeAxes = Axes.Both, Name = "Content", Anchor = Anchor.Centre, Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, } }); - - if (isStrong) - { - Size *= TaikoHitObject.STRONG_CIRCLE_DIAMETER_SCALE; - - //default for symbols etc. - Content.Scale *= TaikoHitObject.STRONG_CIRCLE_DIAMETER_SCALE; - } - } - - protected override void Update() - { - base.Update(); - - //we want to allow for width of content to remain mapped to the area inside us, regardless of the scale applied above. - Content.Width = 1 / Content.Scale.X; } private const float edge_alpha_kiai = 0.5f; private void resetEdgeEffects() { - background.EdgeEffect = new EdgeEffect + background.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = AccentColour.Opacity(KiaiMode ? edge_alpha_kiai : 1f), @@ -166,9 +149,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces double duration = timingPoint.BeatLength * 2; - background.FadeEdgeEffectTo(1, pre_beat_transition_time, EasingTypes.OutQuint); - using (background.BeginDelayedSequence(pre_beat_transition_time)) - background.FadeEdgeEffectTo(edge_alpha_kiai, duration, EasingTypes.OutQuint); + background + .FadeEdgeEffectTo(1, pre_beat_transition_time, Easing.OutQuint) + .Then() + .FadeEdgeEffectTo(edge_alpha_kiai, duration, Easing.OutQuint); } } } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs index f607e2040f..4642e318c4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs @@ -1,25 +1,15 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using osu.Framework.Graphics; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { public class ElongatedCirclePiece : CirclePiece { - /// - /// As we are being used to define the absolute size of hits, we need to be given a relative reference of our containing playfield container. - /// - public Func PlayfieldLengthReference; - - /// - /// The length of this piece as a multiple of the value returned by - /// - public float Length; - - public ElongatedCirclePiece(bool isStrong = false) : base(isStrong) + public ElongatedCirclePiece() { + RelativeSizeAxes = Axes.Y; } protected override void Update() @@ -34,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces Right = padding, }; - Width = (PlayfieldLengthReference?.Invoke() ?? 0) * Length + DrawHeight; + Width = Parent.DrawSize.X + DrawHeight; } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs index 4146edbdf7..60224a291d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitSymbolPiece.cs @@ -3,9 +3,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { @@ -18,7 +18,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { Anchor = Anchor.Centre; Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; Size = new Vector2(CirclePiece.SYMBOL_SIZE); + BorderThickness = CirclePiece.SYMBOL_BORDER; BorderColour = Color4.White; Masking = true; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs index 0f703837a9..c3fdc671a4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs @@ -1,7 +1,9 @@ // 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.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces @@ -9,16 +11,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces /// /// The symbol used for swell pieces. /// - public class SwellSymbolPiece : TextAwesome + public class SwellSymbolPiece : Container { public SwellSymbolPiece() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - UseFullGlyphHeight = true; - TextSize = CirclePiece.SYMBOL_INNER_SIZE; - Icon = FontAwesome.fa_asterisk; - Shadow = false; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(CirclePiece.SYMBOL_SIZE); + Padding = new MarginPadding(CirclePiece.SYMBOL_BORDER); + + Children = new[] + { + new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.fa_asterisk, + Shadow = false + } + }; } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs index 5e7e9e6350..4b40bbf384 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TaikoPiece.cs @@ -2,9 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Graphics; -using OpenTK; using OpenTK.Graphics; using osu.Game.Graphics.Containers; +using osu.Framework.Graphics; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { @@ -35,8 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public TaikoPiece() { - //just a default - Size = new Vector2(TaikoHitObject.DEFAULT_CIRCLE_DIAMETER); + RelativeSizeAxes = Axes.Both; } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs index 1a0d0156e8..211bf910a4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs @@ -3,9 +3,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces { @@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces /// Any tick that is not the first for a drumroll is not filled, but is instead displayed /// as a hollow circle. This is what controls the border width of that circle. /// - private const float tick_border_width = TaikoHitObject.DEFAULT_CIRCLE_DIAMETER / 16; + private const float tick_border_width = 5; /// /// The size of a tick. /// - private const float tick_size = TaikoHitObject.DEFAULT_CIRCLE_DIAMETER / 6; + private const float tick_size = 0.35f; private bool filled; public bool Filled @@ -37,6 +37,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces public TickPiece() { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; Size = new Vector2(tick_size); Add(new CircularContainer diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 18e3016fc3..526d23f51a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -5,8 +5,8 @@ using osu.Game.Rulesets.Objects.Types; using System; using System.Collections.Generic; using System.Linq; -using osu.Game.Database; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Taiko.Objects @@ -80,7 +80,6 @@ namespace osu.Game.Rulesets.Taiko.Objects ret.Add(new DrumRollTick { FirstTick = first, - ScrollTime = ScrollTime, TickSpacing = tickSpacing, StartTime = t, IsStrong = IsStrong, diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index f31472d0fd..03b9be4157 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.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.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Database; namespace osu.Game.Rulesets.Taiko.Objects { diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 79bb901112..f1c0afc675 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -1,44 +1,28 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Database; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Taiko.UI; namespace osu.Game.Rulesets.Taiko.Objects { public abstract class TaikoHitObject : HitObject { /// - /// Diameter of a circle relative to the size of the . + /// Default size of a drawable taiko hit object. /// - public const float PLAYFIELD_RELATIVE_DIAMETER = 0.45f; + public const float DEFAULT_SIZE = 0.45f; /// - /// Scale multiplier for a strong circle. + /// Scale multiplier for a strong drawable taiko hit object. /// - public const float STRONG_CIRCLE_DIAMETER_SCALE = 1.4f; + public const float STRONG_SCALE = 1.4f; /// - /// Default circle diameter. + /// Default size of a strong drawable taiko hit object. /// - public const float DEFAULT_CIRCLE_DIAMETER = TaikoPlayfield.DEFAULT_PLAYFIELD_HEIGHT * PLAYFIELD_RELATIVE_DIAMETER; - - /// - /// Default strong circle diameter. - /// - public const float DEFAULT_STRONG_CIRCLE_DIAMETER = DEFAULT_CIRCLE_DIAMETER * STRONG_CIRCLE_DIAMETER_SCALE; - - /// - /// The time taken from the initial (off-screen) spawn position to the centre of the hit target for a of 1000ms. - /// - private const double scroll_time = 6000; - - /// - /// Our adjusted taking into consideration local and other speed multipliers. - /// - public double ScrollTime; + public const float DEFAULT_STRONG_SIZE = DEFAULT_SIZE * STRONG_SCALE; /// /// Whether this HitObject is a "strong" type. @@ -55,12 +39,8 @@ namespace osu.Game.Rulesets.Taiko.Objects { base.ApplyDefaults(controlPointInfo, difficulty); - TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime); - ScrollTime = scroll_time * (timingPoint.BeatLength * difficultyPoint.SpeedMultiplier / 1000) / difficulty.SliderMultiplier; - Kiai |= effectPoint.KiaiMode; } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index f5e2094cbf..647a1381c6 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -3,7 +3,6 @@ using System; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; @@ -114,8 +113,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring { } - public TaikoScoreProcessor(HitRenderer hitRenderer) - : base(hitRenderer) + public TaikoScoreProcessor(RulesetContainer rulesetContainer) + : base(rulesetContainer) { } diff --git a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs index 33e9510f1c..35f849c704 100644 --- a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs @@ -135,6 +135,6 @@ namespace osu.Game.Rulesets.Taiko return difficulty; } - protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(true); } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/TaikoInputManager.cs b/osu.Game.Rulesets.Taiko/TaikoInputManager.cs new file mode 100644 index 0000000000..1e3c0fbcf2 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/TaikoInputManager.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Taiko +{ + public class TaikoInputManager : RulesetInputManager + { + public TaikoInputManager(RulesetInfo ruleset) + : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + } + + public enum TaikoAction + { + [Description("Left (Rim)")] + LeftRim, + [Description("Left (Centre)")] + LeftCentre, + [Description("Right (Centre)")] + RightCentre, + [Description("Right (Rim)")] + RightRim + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 7c169f820b..f457732085 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -1,23 +1,35 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Scoring; +using osu.Framework.Input.Bindings; namespace osu.Game.Rulesets.Taiko { public class TaikoRuleset : Ruleset { - public override HitRenderer CreateHitRendererWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new TaikoHitRenderer(beatmap, isForCurrentRuleset); + public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new TaikoRulesetContainer(this, beatmap, isForCurrentRuleset); + + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] + { + new KeyBinding(InputKey.D, TaikoAction.LeftRim), + new KeyBinding(InputKey.F, TaikoAction.LeftCentre), + new KeyBinding(InputKey.J, TaikoAction.RightCentre), + new KeyBinding(InputKey.K, TaikoAction.RightRim), + new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), + new KeyBinding(InputKey.MouseLeft, TaikoAction.RightCentre), + new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim), + new KeyBinding(InputKey.MouseRight, TaikoAction.RightRim), + }; public override IEnumerable GetModsFor(ModType type) { @@ -28,7 +40,14 @@ namespace osu.Game.Rulesets.Taiko { new TaikoModEasy(), new TaikoModNoFail(), - new TaikoModHalfTime(), + new MultiMod + { + Mods = new Mod[] + { + new TaikoModHalfTime(), + new TaikoModDaycore(), + }, + }, }; case ModType.DifficultyIncrease: @@ -76,22 +95,21 @@ namespace osu.Game.Rulesets.Taiko } } + public override Mod GetAutoplayMod() => new TaikoModAutoplay(); + public override string Description => "osu!taiko"; - public override FontAwesome Icon => FontAwesome.fa_osu_taiko_o; - - public override IEnumerable CreateGameplayKeys() => new KeyCounter[] - { - new KeyCounterKeyboard(Key.D), - new KeyCounterKeyboard(Key.F), - new KeyCounterKeyboard(Key.J), - new KeyCounterKeyboard(Key.K) - }; + public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o }; public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new TaikoDifficultyCalculator(beatmap); public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); public override int LegacyID => 1; + + public TaikoRuleset(RulesetInfo rulesetInfo) + : base(rulesetInfo) + { + } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index 08fd8dbecc..779471b8dc 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -6,6 +6,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; +using osu.Framework.Graphics; namespace osu.Game.Rulesets.Taiko.UI { @@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.UI switch (Judgement.Result) { case HitResult.Hit: - MoveToY(-100, 500); + this.MoveToY(-100, 500); break; } diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index c0c329c870..cb849a11c7 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -6,7 +6,7 @@ using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; @@ -30,10 +30,11 @@ namespace osu.Game.Rulesets.Taiko.UI Judgement = judgement; - Anchor = Anchor.Centre; + Anchor = Anchor.CentreLeft; Origin = Anchor.Centre; - Size = new Vector2(TaikoPlayfield.HIT_TARGET_OFFSET + TaikoHitObject.DEFAULT_CIRCLE_DIAMETER); + RelativeSizeAxes = Axes.Both; + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE); RelativePositionAxes = Axes.Both; @@ -62,8 +63,8 @@ namespace osu.Game.Rulesets.Taiko.UI { base.LoadComplete(); - ScaleTo(3f, 1000, EasingTypes.OutQuint); - FadeOut(500); + this.ScaleTo(3f, 1000, Easing.OutQuint); + this.FadeOut(500); Expire(); } @@ -73,7 +74,7 @@ namespace osu.Game.Rulesets.Taiko.UI /// public void VisualiseSecondHit() { - ResizeTo(new Vector2(TaikoPlayfield.HIT_TARGET_OFFSET + TaikoHitObject.DEFAULT_STRONG_CIRCLE_DIAMETER), 50); + this.ResizeTo(new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), 50); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs b/osu.Game.Rulesets.Taiko/UI/HitTarget.cs index fde2623246..8f3b6840f3 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitTarget.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitTarget.cs @@ -5,7 +5,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.UI @@ -15,11 +15,6 @@ namespace osu.Game.Rulesets.Taiko.UI /// internal class HitTarget : Container { - /// - /// The 1px inner border of the taiko playfield. - /// - private const float border_offset = 1; - /// /// Thickness of all drawn line pieces. /// @@ -27,8 +22,6 @@ namespace osu.Game.Rulesets.Taiko.UI public HitTarget() { - Size = new Vector2(TaikoPlayfield.DEFAULT_PLAYFIELD_HEIGHT); - Children = new Drawable[] { new Box @@ -36,8 +29,8 @@ namespace osu.Game.Rulesets.Taiko.UI Name = "Bar Upper", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Y = border_offset, - Size = new Vector2(border_thickness, (TaikoPlayfield.DEFAULT_PLAYFIELD_HEIGHT - TaikoHitObject.DEFAULT_STRONG_CIRCLE_DIAMETER) / 2f - border_offset), + RelativeSizeAxes = Axes.Y, + Size = new Vector2(border_thickness, (1 - TaikoHitObject.DEFAULT_STRONG_SIZE) / 2f), Alpha = 0.1f }, new CircularContainer @@ -45,7 +38,9 @@ namespace osu.Game.Rulesets.Taiko.UI Name = "Strong Hit Ring", Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_CIRCLE_DIAMETER), + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), Masking = true, BorderColour = Color4.White, BorderThickness = border_thickness, @@ -65,7 +60,9 @@ namespace osu.Game.Rulesets.Taiko.UI Name = "Normal Hit Ring", Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(TaikoHitObject.DEFAULT_CIRCLE_DIAMETER), + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE), Masking = true, BorderColour = Color4.White, BorderThickness = border_thickness, @@ -85,8 +82,8 @@ namespace osu.Game.Rulesets.Taiko.UI Name = "Bar Lower", Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Y = -border_offset, - Size = new Vector2(border_thickness, (TaikoPlayfield.DEFAULT_PLAYFIELD_HEIGHT - TaikoHitObject.DEFAULT_STRONG_CIRCLE_DIAMETER) / 2f - border_offset), + RelativeSizeAxes = Axes.Y, + Size = new Vector2(border_thickness, (1 - TaikoHitObject.DEFAULT_STRONG_SIZE) / 2f), Alpha = 0.1f }, }; diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 999d76ab0b..659f421ebe 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -3,13 +3,12 @@ using System; using OpenTK; -using OpenTK.Input; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Game.Graphics; namespace osu.Game.Rulesets.Taiko.UI @@ -21,9 +20,10 @@ namespace osu.Game.Rulesets.Taiko.UI { public InputDrum() { - Size = new Vector2(TaikoPlayfield.DEFAULT_PLAYFIELD_HEIGHT); + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; - const float middle_split = 10; + const float middle_split = 0.025f; Children = new Drawable[] { @@ -33,9 +33,10 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, X = -middle_split / 2, - RimKey = Key.D, - CentreKey = Key.F + RimAction = TaikoAction.LeftRim, + CentreAction = TaikoAction.LeftCentre }, new TaikoHalfDrum(true) { @@ -43,10 +44,10 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, X = middle_split / 2, - Position = new Vector2(-1f, 0), - RimKey = Key.K, - CentreKey = Key.J + RimAction = TaikoAction.RightRim, + CentreAction = TaikoAction.RightCentre } }; } @@ -54,17 +55,17 @@ namespace osu.Game.Rulesets.Taiko.UI /// /// A half-drum. Contains one centre and one rim hit. /// - private class TaikoHalfDrum : Container + private class TaikoHalfDrum : Container, IKeyBindingHandler { /// /// The key to be used for the rim of the half-drum. /// - public Key RimKey; + public TaikoAction RimAction; /// /// The key to be used for the centre of the half-drum. /// - public Key CentreKey; + public TaikoAction CentreAction; private readonly Sprite rim; private readonly Sprite rimHit; @@ -122,20 +123,17 @@ namespace osu.Game.Rulesets.Taiko.UI centreHit.Colour = colours.Pink; } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + public bool OnPressed(TaikoAction action) { - if (args.Repeat) - return false; - Drawable target = null; Drawable back = null; - if (args.Key == CentreKey) + if (action == CentreAction) { target = centreHit; back = centre; } - else if (args.Key == RimKey) + else if (action == RimAction) { target = rimHit; back = rim; @@ -149,19 +147,23 @@ namespace osu.Game.Rulesets.Taiko.UI const float down_time = 40; const float up_time = 1000; - back.ScaleTo(target.Scale.X - scale_amount, down_time, EasingTypes.OutQuint); - back.Delay(down_time); - back.ScaleTo(1, up_time, EasingTypes.OutQuint); + back.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint) + .Then() + .ScaleTo(1, up_time, Easing.OutQuint); - target.ScaleTo(target.Scale.X - scale_amount, down_time, EasingTypes.OutQuint); - target.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, EasingTypes.OutQuint); - target.Delay(down_time); - target.ScaleTo(1, up_time, EasingTypes.OutQuint); - target.FadeOut(up_time, EasingTypes.OutQuint); + target.Animate( + t => t.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint), + t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, Easing.OutQuint) + ).Then( + t => t.ScaleTo(1, up_time, Easing.OutQuint), + t => t.FadeOut(up_time, Easing.OutQuint) + ); } return false; } + + public bool OnReleased(TaikoAction action) => false; } } } diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index e0da3ed3db..bac956a25b 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -5,7 +5,7 @@ using OpenTK; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; @@ -24,11 +24,11 @@ namespace osu.Game.Rulesets.Taiko.UI Judgement = judgement; - Anchor = Anchor.Centre; + Anchor = Anchor.CentreLeft; Origin = Anchor.Centre; - RelativeSizeAxes = Axes.Y; - Size = new Vector2(TaikoHitObject.DEFAULT_CIRCLE_DIAMETER, 1); + RelativeSizeAxes = Axes.Both; + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1); Masking = true; Alpha = 0.25f; @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load(OsuColour colours) { - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = isRim ? colours.BlueDarker : colours.PinkDarker, @@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Taiko.UI { base.LoadComplete(); - ScaleTo(new Vector2(1, 3f), 500, EasingTypes.OutQuint); - FadeOut(250); + this.ScaleTo(new Vector2(1, 3f), 500, Easing.OutQuint); + this.FadeOut(250); Expire(); } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index c7bd4a6704..678de7f713 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -3,7 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; using OpenTK; @@ -15,37 +15,38 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Extensions.Color4Extensions; using System.Linq; using osu.Game.Rulesets.Taiko.Objects.Drawables; -using System; namespace osu.Game.Rulesets.Taiko.UI { - public class TaikoPlayfield : Playfield + public class TaikoPlayfield : ScrollingPlayfield { /// - /// The default play field height. + /// Default height of a when inside a . /// - public const float DEFAULT_PLAYFIELD_HEIGHT = 178f; + public const float DEFAULT_HEIGHT = 178; /// /// The offset from which the center of the hit target lies at. /// - public const float HIT_TARGET_OFFSET = TaikoHitObject.DEFAULT_STRONG_CIRCLE_DIAMETER / 2f + 40; + public const float HIT_TARGET_OFFSET = 100; /// /// The size of the left area of the playfield. This area contains the input drum. /// private const float left_area_size = 240; - protected override Container Content => hitObjectContainer; private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; - private readonly Container barLineContainer; private readonly Container judgementContainer; - private readonly Container hitObjectContainer; + protected override Container Content => content; + private readonly Container content; + private readonly Container topLevelHitContainer; + private readonly Container barlineContainer; + private readonly Container overlayBackgroundContainer; private readonly Container backgroundContainer; @@ -53,115 +54,132 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Box background; public TaikoPlayfield() + : base(Axes.X) { - AddInternal(new Drawable[] + AddRangeInternal(new Drawable[] { - new ScaleFixContainer + backgroundContainer = new Container { - RelativeSizeAxes = Axes.X, - Height = DEFAULT_PLAYFIELD_HEIGHT, - Children = new[] + Name = "Transparent playfield background", + RelativeSizeAxes = Axes.Both, + Masking = true, + EdgeEffect = new EdgeEffectParameters { - backgroundContainer = new Container + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.2f), + Radius = 5, + }, + Children = new Drawable[] + { + background = new Box { - Name = "Transparent playfield background", RelativeSizeAxes = Axes.Both, - BorderThickness = 2, - Masking = true, - EdgeEffect = new EdgeEffect - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.2f), - Radius = 5, - }, - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.6f - }, - } + Alpha = 0.6f }, + } + }, + new Container + { + Name = "Right area", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = left_area_size }, + Children = new Drawable[] + { new Container { - Name = "Right area", + Name = "Masked elements before hit objects", RelativeSizeAxes = Axes.Both, - Margin = new MarginPadding { Left = left_area_size }, + Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, + Masking = true, Children = new Drawable[] { - new Container - { - Name = "Masked elements", - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, - Masking = true, - Children = new Drawable[] - { - hitExplosionContainer = new Container - { - RelativeSizeAxes = Axes.Y, - BlendingMode = BlendingMode.Additive, - }, - barLineContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - new HitTarget - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - }, - hitObjectContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - } - }, - kiaiExplosionContainer = new Container - { - Name = "Kiai hit explosions", - RelativeSizeAxes = Axes.Y, - Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, - BlendingMode = BlendingMode.Additive - }, - judgementContainer = new Container - { - Name = "Judgements", - RelativeSizeAxes = Axes.Y, - Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, - BlendingMode = BlendingMode.Additive - }, - } - }, - overlayBackgroundContainer = new Container - { - Name = "Left overlay", - Size = new Vector2(left_area_size, DEFAULT_PLAYFIELD_HEIGHT), - BorderThickness = 1, - Children = new Drawable[] - { - overlayBackground = new Box + hitExplosionContainer = new Container { RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + BlendingMode = BlendingMode.Additive, }, - new InputDrum + new HitTarget { - Anchor = Anchor.Centre, + Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - Position = new Vector2(0.10f, 0), - Scale = new Vector2(0.9f) - }, - new Box - { - Anchor = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = 10, - ColourInfo = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)), - }, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit + } } }, + barlineContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = HIT_TARGET_OFFSET } + }, + content = new Container + { + Name = "Hit objects", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, + Masking = true + }, + kiaiExplosionContainer = new Container + { + Name = "Kiai hit explosions", + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, + BlendingMode = BlendingMode.Additive + }, + judgementContainer = new Container + { + Name = "Judgements", + RelativeSizeAxes = Axes.Y, + Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, + BlendingMode = BlendingMode.Additive + }, + } + }, + overlayBackgroundContainer = new Container + { + Name = "Left overlay", + RelativeSizeAxes = Axes.Y, + Size = new Vector2(left_area_size, 1), + Children = new Drawable[] + { + overlayBackground = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new InputDrum + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Scale = new Vector2(0.9f), + Margin = new MarginPadding { Right = 20 } + }, + new Box + { + Anchor = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = 10, + Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)), + }, + } + }, + new Container + { + Name = "Border", + RelativeSizeAxes = Axes.Both, + Masking = true, + MaskingSmoothness = 0, + BorderThickness = 2, + AlwaysPresent = true, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } } }, topLevelHitContainer = new Container @@ -170,6 +188,8 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, } }); + + VisibleTimeRange.Value = 6000; } [BackgroundDependencyLoader] @@ -188,17 +208,16 @@ namespace osu.Game.Rulesets.Taiko.UI base.Add(h); + var barline = h as DrawableBarLine; + if (barline != null) + barlineContainer.Add(barline.CreateProxy()); + // Swells should be moved at the very top of the playfield when they reach the hit target var swell = h as DrawableSwell; if (swell != null) swell.OnStart += () => topLevelHitContainer.Add(swell.CreateProxy()); } - public void AddBarLine(DrawableBarLine barLine) - { - barLineContainer.Add(barLine); - } - public override void OnJudgement(DrawableHitObject judgedObject) { bool wasHit = judgedObject.Judgement.Result == HitResult.Hit; @@ -219,7 +238,7 @@ namespace osu.Game.Rulesets.Taiko.UI if (!secondHit) { - if (judgedObject.X >= -0.05f && !(judgedObject is DrawableSwell)) + if (judgedObject.X >= -0.05f && judgedObject is DrawableHit) { // If we're far enough away from the left stage, we should bring outselves in front of it topLevelHitContainer.Add(judgedObject.CreateProxy()); @@ -229,61 +248,9 @@ namespace osu.Game.Rulesets.Taiko.UI if (judgedObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject.Judgement, isRim)); - } else hitExplosionContainer.Children.FirstOrDefault(e => e.Judgement == judgedObject.Judgement)?.VisualiseSecondHit(); } - - /// - /// This is a very special type of container. It serves a similar purpose to , however unlike , - /// this will only adjust the scale relative to the height of its parent and will maintain the original width relative to its parent. - /// - /// - /// By adjusting the scale relative to the height of its parent, the aspect ratio of this container's children is maintained, however this is undesirable - /// in the case where the hit object container should not have its width adjusted by scale. To counteract this, another container is nested inside this - /// container which takes care of reversing the width adjustment while appearing transparent to the user. - /// - /// - private class ScaleFixContainer : Container - { - protected override Container Content => widthAdjustmentContainer; - private readonly WidthAdjustmentContainer widthAdjustmentContainer; - - /// - /// We only want to apply DrawScale in the Y-axis to preserve aspect ratio and doesn't care about having its width adjusted. - /// - protected override Vector2 DrawScale => Scale * RelativeToAbsoluteFactor.Y / DrawHeight; - - public ScaleFixContainer() - { - AddInternal(widthAdjustmentContainer = new WidthAdjustmentContainer { ParentDrawScaleReference = () => DrawScale.X }); - } - - /// - /// The container type that reverses the width adjustment. - /// - private class WidthAdjustmentContainer : Container - { - /// - /// This container needs to know its parent's so it can reverse the width adjustment caused by . - /// - public Func ParentDrawScaleReference; - - public WidthAdjustmentContainer() - { - // This container doesn't care about height, it should always fill its parent - RelativeSizeAxes = Axes.Y; - } - - protected override void Update() - { - base.Update(); - - // Reverse the DrawScale adjustment - Width = Parent.DrawSize.X / ParentDrawScaleReference(); - } - } - } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoHitRenderer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs similarity index 84% rename from osu.Game.Rulesets.Taiko/UI/TaikoHitRenderer.cs rename to osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 662cace511..96e5df12cd 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoHitRenderer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -18,13 +18,14 @@ using osu.Game.Rulesets.Taiko.Replays; using OpenTK; using osu.Game.Rulesets.Beatmaps; using System.Linq; +using osu.Framework.Input; namespace osu.Game.Rulesets.Taiko.UI { - public class TaikoHitRenderer : HitRenderer + public class TaikoRulesetContainer : ScrollingRulesetContainer { - public TaikoHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset) - : base(beatmap, isForCurrentRuleset) + public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(ruleset, beatmap, isForCurrentRuleset) { } @@ -36,11 +37,6 @@ namespace osu.Game.Rulesets.Taiko.UI private void loadBarLines() { - var taikoPlayfield = Playfield as TaikoPlayfield; - - if (taikoPlayfield == null) - return; - TaikoHitObject lastObject = Beatmap.HitObjects[Beatmap.HitObjects.Count - 1]; double lastHitTime = 1 + (lastObject as IHasEndTime)?.EndTime ?? lastObject.StartTime; @@ -72,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.UI barLine.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.Difficulty); bool isMajor = currentBeat % (int)currentPoint.TimeSignature == 0; - taikoPlayfield.AddBarLine(isMajor ? new DrawableBarLineMajor(barLine) : new DrawableBarLine(barLine)); + Playfield.Add(isMajor ? new DrawableBarLineMajor(barLine) : new DrawableBarLine(barLine)); double bl = currentPoint.BeatLength; if (bl < 800) @@ -85,7 +81,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected override Vector2 GetPlayfieldAspectAdjust() { - const float default_relative_height = TaikoPlayfield.DEFAULT_PLAYFIELD_HEIGHT / 768; + const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; const float default_aspect = 16f / 9f; float aspectAdjust = MathHelper.Clamp(DrawWidth / DrawHeight, 0.4f, 4) / default_aspect; @@ -93,10 +89,11 @@ namespace osu.Game.Rulesets.Taiko.UI return new Vector2(1, default_relative_height * aspectAdjust); } - public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this); - protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(IsForCurrentRuleset); + + public override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); protected override Playfield CreatePlayfield() => new TaikoPlayfield { diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index 8d6fcb503c..33748a267f 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -33,8 +33,9 @@ false - - $(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll + + $(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll + True @@ -85,12 +86,13 @@ + - + diff --git a/osu.Game.Rulesets.Taiko/packages.config b/osu.Game.Rulesets.Taiko/packages.config index dc059c684b..8add43d5d5 100644 --- a/osu.Game.Rulesets.Taiko/packages.config +++ b/osu.Game.Rulesets.Taiko/packages.config @@ -4,5 +4,5 @@ Copyright (c) 2007-2017 ppy Pty Ltd . Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE --> - + \ No newline at end of file diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuLegacyDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuLegacyDecoderTest.cs index 4814af984e..da3b448f74 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuLegacyDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuLegacyDecoderTest.cs @@ -16,12 +16,6 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class OsuLegacyDecoderTest { - [OneTimeSetUpAttribute] - public void SetUp() - { - OsuLegacyDecoder.Register(); - } - [Test] public void TestDecodeMetadata() { diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 7fb01cedc0..ecaf0db096 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -10,8 +10,10 @@ using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Desktop.Platform; using osu.Framework.Platform; -using osu.Game.Database; using osu.Game.IPC; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; namespace osu.Game.Tests.Beatmaps.IO { @@ -26,15 +28,15 @@ namespace osu.Game.Tests.Beatmaps.IO //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new HeadlessGameHost()) { - loadOsu(host); + var osu = loadOsu(host); var temp = prepareTempCopy(osz_path); Assert.IsTrue(File.Exists(temp)); - host.Dependencies.Get().Import(temp); + osu.Dependencies.Get().Import(temp); - ensureLoaded(host); + ensureLoaded(osu); Assert.IsFalse(File.Exists(temp)); } @@ -49,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(host.IsPrimaryInstance); Assert.IsTrue(!client.IsPrimaryInstance); - loadOsu(host); + var osu = loadOsu(host); var temp = prepareTempCopy(osz_path); @@ -59,7 +61,7 @@ namespace osu.Game.Tests.Beatmaps.IO if (!importer.ImportAsync(temp).Wait(10000)) Assert.Fail(@"IPC took too long to send"); - ensureLoaded(host); + ensureLoaded(osu); Assert.IsFalse(File.Exists(temp)); } @@ -71,22 +73,20 @@ namespace osu.Game.Tests.Beatmaps.IO //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new HeadlessGameHost()) { - loadOsu(host); + var osu = loadOsu(host); var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); + Assert.IsTrue(File.Exists(temp), "Temporary file copy never substantiated"); using (File.OpenRead(temp)) - host.Dependencies.Get().Import(temp); + osu.Dependencies.Get().Import(temp); - ensureLoaded(host); - - Assert.IsTrue(File.Exists(temp)); + ensureLoaded(osu); File.Delete(temp); - Assert.IsFalse(File.Exists(temp)); + Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); } } @@ -105,20 +105,21 @@ namespace osu.Game.Tests.Beatmaps.IO Thread.Sleep(1); //reset beatmap database (sqlite and storage backing) - host.Dependencies.Get().Reset(); - host.Dependencies.Get().Reset(); + osu.Dependencies.Get().Reset(); + osu.Dependencies.Get().Reset(); return osu; } - private void ensureLoaded(GameHost host, int timeout = 10000) + private void ensureLoaded(OsuGameBase osu, int timeout = 60000) { IEnumerable resultSets = null; + var store = osu.Dependencies.Get(); + Action waitAction = () => { - while (!(resultSets = host.Dependencies.Get() - .Query().Where(s => s.OnlineBeatmapSetID == 241526)).Any()) + while (!(resultSets = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526)).Any()) Thread.Sleep(50); }; @@ -134,15 +135,14 @@ namespace osu.Game.Tests.Beatmaps.IO //if we don't re-check here, the set will be inserted but the beatmaps won't be present yet. waitAction = () => { - while ((resultBeatmaps = host.Dependencies.Get() - .GetAllWithChildren(s => s.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0)).Count() != 12) + while ((resultBeatmaps = store.QueryBeatmaps(s => s.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0)).Count() != 12) Thread.Sleep(50); }; Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(timeout), @"Beatmaps did not import to the database in allocated time"); - var set = host.Dependencies.Get().GetChildren(resultSets.First()); + var set = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526).First(); Assert.IsTrue(set.Beatmaps.Count == resultBeatmaps.Count(), $@"Incorrect database beatmap count post-import ({resultBeatmaps.Count()} but should be {set.Beatmaps.Count})."); @@ -152,18 +152,17 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(set.Beatmaps.Count > 0); - var beatmap = host.Dependencies.Get().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap; + var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = host.Dependencies.Get().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap; + beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = host.Dependencies.Get().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap; + beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = host.Dependencies.Get().GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap; + beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); } } } - diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 03d09e24e0..7a7a8a58bc 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -2,23 +2,18 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.IO; +using System.Linq; using NUnit.Framework; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.IO; using osu.Game.Tests.Resources; using osu.Game.Beatmaps.Formats; -using osu.Game.Database; namespace osu.Game.Tests.Beatmaps.IO { [TestFixture] public class OszArchiveReaderTest { - [OneTimeSetUpAttribute] - public void SetUp() - { - OszArchiveReader.Register(); - } - [Test] public void TestReadBeatmaps() { @@ -40,7 +35,7 @@ namespace osu.Game.Tests.Beatmaps.IO "Soleily - Renatus (MMzz) [Muzukashii].osu", "Soleily - Renatus (MMzz) [Oni].osu" }; - var maps = reader.BeatmapFilenames; + var maps = reader.Filenames.ToArray(); foreach (var map in expected) Assert.Contains(map, maps); } @@ -86,4 +81,3 @@ namespace osu.Game.Tests.Beatmaps.IO } } } - diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index b8fcb80aaf..8ec68b41be 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -30,11 +30,13 @@ false - - $(SolutionDir)\packages\NUnit.3.6.1\lib\net45\nunit.framework.dll + + $(SolutionDir)\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll + True - - $(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll + + $(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll + True diff --git a/osu.Game.Tests/packages.config b/osu.Game.Tests/packages.config index 9972fb41a1..9ad76308d7 100644 --- a/osu.Game.Tests/packages.config +++ b/osu.Game.Tests/packages.config @@ -4,8 +4,8 @@ Copyright (c) 2007-2017 ppy Pty Ltd . Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE --> - - + + \ No newline at end of file diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 0368455b92..82777734bb 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -3,11 +3,11 @@ using OpenTK.Graphics; using osu.Game.Beatmaps.Timing; -using osu.Game.Database; using osu.Game.Rulesets.Objects; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.IO.Serialization; namespace osu.Game.Beatmaps { @@ -46,7 +46,7 @@ namespace osu.Game.Beatmaps /// The original beatmap to use the parameters of. public Beatmap(Beatmap original = null) { - BeatmapInfo = original?.BeatmapInfo ?? BeatmapInfo; + BeatmapInfo = original?.BeatmapInfo.DeepClone() ?? BeatmapInfo; ControlPointInfo = original?.ControlPointInfo ?? ControlPointInfo; Breaks = original?.Breaks ?? Breaks; ComboColors = original?.ComboColors ?? ComboColors; diff --git a/osu.Game/Database/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs similarity index 96% rename from osu.Game/Database/BeatmapDifficulty.cs rename to osu.Game/Beatmaps/BeatmapDifficulty.cs index cf1305f705..7c2294cae9 100644 --- a/osu.Game/Database/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -3,7 +3,7 @@ using SQLite.Net.Attributes; -namespace osu.Game.Database +namespace osu.Game.Beatmaps { public class BeatmapDifficulty { @@ -39,4 +39,3 @@ namespace osu.Game.Database } } } - diff --git a/osu.Game/Database/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs similarity index 85% rename from osu.Game/Database/BeatmapInfo.cs rename to osu.Game/Beatmaps/BeatmapInfo.cs index 9f253f6055..ebf77bf9df 100644 --- a/osu.Game/Database/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -1,14 +1,15 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using Newtonsoft.Json; -using osu.Game.IO.Serialization; -using SQLite.Net.Attributes; -using SQLiteNetExtensions.Attributes; using System; using System.Linq; +using Newtonsoft.Json; +using osu.Game.IO.Serialization; +using osu.Game.Rulesets; +using SQLite.Net.Attributes; +using SQLiteNetExtensions.Attributes; -namespace osu.Game.Database +namespace osu.Game.Beatmaps { public class BeatmapInfo : IEquatable, IJsonSerializable { @@ -48,9 +49,16 @@ namespace osu.Game.Database public string Path { get; set; } - [JsonProperty("file_md5")] + [JsonProperty("file_sha2")] public string Hash { get; set; } + /// + /// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.). + /// + [Indexed] + [JsonProperty("file_md5")] + public string MD5Hash { get; set; } + // General public int AudioLeadIn { get; set; } public bool Countdown { get; set; } @@ -60,7 +68,7 @@ namespace osu.Game.Database [ForeignKey(typeof(RulesetInfo))] public int RulesetID { get; set; } - [OneToOne(CascadeOperations = CascadeOperation.All)] + [OneToOne(CascadeOperations = CascadeOperation.CascadeRead)] public RulesetInfo Ruleset { get; set; } public bool LetterboxInBreaks { get; set; } @@ -68,7 +76,7 @@ namespace osu.Game.Database // Editor // This bookmarks stuff is necessary because DB doesn't know how to store int[] - public string StoredBookmarks { get; internal set; } + public string StoredBookmarks { get; set; } [Ignore] [JsonIgnore] @@ -94,11 +102,11 @@ namespace osu.Game.Database } public bool AudioEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && - BeatmapSet.Path == other.BeatmapSet.Path && + BeatmapSet.Hash == other.BeatmapSet.Hash && (Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile; public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && - BeatmapSet.Path == other.BeatmapSet.Path && + BeatmapSet.Hash == other.BeatmapSet.Hash && (Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile; } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs new file mode 100644 index 0000000000..1dcab6cb5d --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -0,0 +1,514 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using Ionic.Zip; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Beatmaps.Formats; +using osu.Game.Beatmaps.IO; +using osu.Game.IO; +using osu.Game.IPC; +using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; +using SQLite.Net; + +namespace osu.Game.Beatmaps +{ + /// + /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. + /// + public class BeatmapManager + { + /// + /// Fired when a new becomes available in the database. + /// + public event Action BeatmapSetAdded; + + /// + /// Fired when a is removed from the database. + /// + public event Action BeatmapSetRemoved; + + /// + /// A default representation of a WorkingBeatmap to use when no beatmap is available. + /// + public WorkingBeatmap DefaultBeatmap { private get; set; } + + private readonly Storage storage; + + private readonly FileStore files; + + private readonly SQLiteConnection connection; + + private readonly RulesetStore rulesets; + + private readonly BeatmapStore beatmaps; + + // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) + private BeatmapIPCChannel ipc; + + /// + /// Set an endpoint for notifications to be posted to. + /// + public Action PostNotification { private get; set; } + + /// + /// Set a storage with access to an osu-stable install for import purposes. + /// + public Func GetStableStorage { private get; set; } + + public BeatmapManager(Storage storage, FileStore files, SQLiteConnection connection, RulesetStore rulesets, IIpcHost importHost = null) + { + beatmaps = new BeatmapStore(connection); + beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); + beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); + + this.storage = storage; + this.files = files; + this.connection = connection; + this.rulesets = rulesets; + + if (importHost != null) + ipc = new BeatmapIPCChannel(importHost, this); + } + + /// + /// Import one or more from filesystem . + /// This will post a notification tracking import progress. + /// + /// One or more beatmap locations on disk. + public void Import(params string[] paths) + { + var notification = new ProgressNotification + { + Text = "Beatmap import is initialising...", + Progress = 0, + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + int i = 0; + foreach (string path in paths) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + try + { + notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}"; + using (ArchiveReader reader = getReaderFrom(path)) + Import(reader); + + notification.Progress = (float)++i / paths.Length; + + // We may or may not want to delete the file depending on where it is stored. + // e.g. reconstructing/repairing database with beatmaps from default storage. + // Also, not always a single file, i.e. for LegacyFilesystemReader + // TODO: Add a check to prevent files from storage to be deleted. + try + { + if (File.Exists(path)) + File.Delete(path); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})"); + } + } + catch (Exception e) + { + e = e.InnerException ?? e; + Logger.Error(e, @"Could not import beatmap set"); + } + } + + notification.State = ProgressNotificationState.Completed; + } + + private readonly object importLock = new object(); + + /// + /// Import a beatmap from an . + /// + /// The beatmap to be imported. + public BeatmapSetInfo Import(ArchiveReader archiveReader) + { + BeatmapSetInfo set = null; + + // let's only allow one concurrent import at a time for now. + lock (importLock) + connection.RunInTransaction(() => Import(set = importToStorage(archiveReader))); + + return set; + } + + /// + /// Import a beatmap from a . + /// + /// The beatmap to be imported. + public void Import(BeatmapSetInfo beatmapSetInfo) + { + // If we have an ID then we already exist in the database. + if (beatmapSetInfo.ID != 0) return; + + lock (beatmaps) + beatmaps.Add(beatmapSetInfo); + } + + /// + /// Delete a beatmap from the manager. + /// Is a no-op for already deleted beatmaps. + /// + /// The beatmap to delete. + public void Delete(BeatmapSetInfo beatmapSet) + { + lock (beatmaps) + if (!beatmaps.Delete(beatmapSet)) return; + + if (!beatmapSet.Protected) + files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); + } + + /// + /// Returns a to a usable state if it has previously been deleted but not yet purged. + /// Is a no-op for already usable beatmaps. + /// + /// The beatmap to restore. + public void Undelete(BeatmapSetInfo beatmapSet) + { + lock (beatmaps) + if (!beatmaps.Undelete(beatmapSet)) return; + + if (!beatmapSet.Protected) + files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); + } + + /// + /// Retrieve a instance for the provided + /// + /// The beatmap to lookup. + /// The currently loaded . Allows for optimisation where elements are shared with the new beatmap. + /// A instance correlating to the provided . + public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null) + { + if (beatmapInfo == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo) + return DefaultBeatmap; + + lock (beatmaps) + beatmaps.Populate(beatmapInfo); + + if (beatmapInfo.BeatmapSet == null) + throw new InvalidOperationException($@"Beatmap set {beatmapInfo.BeatmapSetInfoID} is not in the local database."); + + if (beatmapInfo.Metadata == null) + beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; + + WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(files.Store, beatmapInfo); + + previous?.TransferTo(working); + + return working; + } + + /// + /// Reset the manager to an empty state. + /// + public void Reset() + { + lock (beatmaps) + beatmaps.Reset(); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapSetInfo QueryBeatmapSet(Func query) + { + lock (beatmaps) + { + BeatmapSetInfo set = beatmaps.Query().FirstOrDefault(query); + + if (set != null) + beatmaps.Populate(set); + + return set; + } + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// Results from the provided query. + public List QueryBeatmapSets(Expression> query) + { + lock (beatmaps) return beatmaps.QueryAndPopulate(query); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapInfo QueryBeatmap(Func query) + { + lock (beatmaps) + { + BeatmapInfo set = beatmaps.Query().FirstOrDefault(query); + + if (set != null) + beatmaps.Populate(set); + + return set; + } + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// Results from the provided query. + public List QueryBeatmaps(Expression> query) + { + lock (beatmaps) return beatmaps.QueryAndPopulate(query); + } + + /// + /// Creates an from a valid storage path. + /// + /// A file or folder path resolving the beatmap content. + /// A reader giving access to the beatmap's content. + private ArchiveReader getReaderFrom(string path) + { + if (ZipFile.IsZipFile(path)) + return new OszArchiveReader(storage.GetStream(path)); + else + return new LegacyFilesystemReader(path); + } + + /// + /// Import a beamap into our local storage. + /// If the beatmap is already imported, the existing instance will be returned. + /// + /// The beatmap archive to be read. + /// The imported beatmap, or an existing instance if it is already present. + private BeatmapSetInfo importToStorage(ArchiveReader reader) + { + // let's make sure there are actually .osu files to import. + string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); + if (string.IsNullOrEmpty(mapName)) + throw new InvalidOperationException("No beatmap files found in the map folder."); + + // for now, concatenate all .osu files in the set to create a unique hash. + MemoryStream hashable = new MemoryStream(); + foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu"))) + using (Stream s = reader.GetStream(file)) + s.CopyTo(hashable); + + var hash = hashable.ComputeSHA2Hash(); + + // check if this beatmap has already been imported and exit early if so. + BeatmapSetInfo beatmapSet; + lock (beatmaps) + beatmapSet = beatmaps.QueryAndPopulate(b => b.Hash == hash).FirstOrDefault(); + + if (beatmapSet != null) + { + Undelete(beatmapSet); + return beatmapSet; + } + + List fileInfos = new List(); + + // import files to manager + foreach (string file in reader.Filenames) + using (Stream s = reader.GetStream(file)) + fileInfos.Add(new BeatmapSetFileInfo + { + Filename = file, + FileInfo = files.Add(s) + }); + + BeatmapMetadata metadata; + + using (var stream = new StreamReader(reader.GetStream(mapName))) + metadata = BeatmapDecoder.GetDecoder(stream).Decode(stream).Metadata; + + beatmapSet = new BeatmapSetInfo + { + OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, + Beatmaps = new List(), + Hash = hash, + Files = fileInfos, + Metadata = metadata + }; + + var mapNames = reader.Filenames.Where(f => f.EndsWith(".osu")); + + foreach (var name in mapNames) + { + using (var raw = reader.GetStream(name)) + using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit + using (var sr = new StreamReader(ms)) + { + raw.CopyTo(ms); + ms.Position = 0; + + var decoder = BeatmapDecoder.GetDecoder(sr); + Beatmap beatmap = decoder.Decode(sr); + + beatmap.BeatmapInfo.Path = name; + beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); + beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); + + // TODO: Diff beatmap metadata with set metadata and leave it here if necessary + beatmap.BeatmapInfo.Metadata = null; + + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.BeatmapInfo.Ruleset = rulesets.Query().FirstOrDefault(r => r.ID == beatmap.BeatmapInfo.RulesetID); + beatmap.BeatmapInfo.StarDifficulty = rulesets.Query().FirstOrDefault(r => r.ID == beatmap.BeatmapInfo.RulesetID)?.CreateInstance()?.CreateDifficultyCalculator(beatmap) + .Calculate() ?? 0; + + beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); + } + } + + return beatmapSet; + } + + /// + /// Returns a list of all usable s. + /// + /// Whether returned objects should be pre-populated with all data. + /// A list of available . + public List GetAllUsableBeatmapSets(bool populate = true) + { + lock (beatmaps) + { + if (populate) + return beatmaps.QueryAndPopulate(b => !b.DeletePending).ToList(); + else + return beatmaps.Query(b => !b.DeletePending).ToList(); + } + } + + protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap + { + private readonly IResourceStore store; + + public BeatmapManagerWorkingBeatmap(IResourceStore store, BeatmapInfo beatmapInfo) + : base(beatmapInfo) + { + this.store = store; + } + + protected override Beatmap GetBeatmap() + { + try + { + Beatmap beatmap; + + BeatmapDecoder decoder; + using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + { + decoder = BeatmapDecoder.GetDecoder(stream); + beatmap = decoder.Decode(stream); + } + + if (beatmap == null || BeatmapSetInfo.StoryboardFile == null) + return beatmap; + + using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) + decoder.Decode(stream, beatmap); + + + return beatmap; + } + catch { return null; } + } + + private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => f.Filename == filename).FileInfo.StoragePath; + + protected override Texture GetBackground() + { + if (Metadata?.BackgroundFile == null) + return null; + + try + { + return new TextureStore(new RawTextureLoaderStore(store), false).Get(getPathForFile(Metadata.BackgroundFile)); + } + catch { return null; } + } + + protected override Track GetTrack() + { + try + { + var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); + return trackData == null ? null : new TrackBass(trackData); + } + catch { return new TrackVirtual(); } + } + } + + /// + /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. + /// + public void ImportFromStable() + { + var stable = GetStableStorage?.Invoke(); + + if (stable == null) + { + Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); + return; + } + + Import(stable.GetDirectories("Songs")); + } + + public void DeleteAll() + { + var maps = GetAllUsableBeatmapSets().ToArray(); + + if (maps.Length == 0) return; + + var notification = new ProgressNotification + { + Progress = 0, + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + int i = 0; + + foreach (var b in maps) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Deleting ({i} of {maps.Length})"; + notification.Progress = (float)++i / maps.Length; + Delete(b); + } + + notification.State = ProgressNotificationState.Completed; + } + } +} diff --git a/osu.Game/Database/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs similarity index 86% rename from osu.Game/Database/BeatmapMetadata.cs rename to osu.Game/Beatmaps/BeatmapMetadata.cs index 04700b3caa..cc9a51b4e2 100644 --- a/osu.Game/Database/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -2,9 +2,10 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; +using Newtonsoft.Json; using SQLite.Net.Attributes; -namespace osu.Game.Database +namespace osu.Game.Beatmaps { public class BeatmapMetadata { @@ -17,8 +18,13 @@ namespace osu.Game.Database public string TitleUnicode { get; set; } public string Artist { get; set; } public string ArtistUnicode { get; set; } + + [JsonProperty(@"creator")] public string Author { get; set; } + public string Source { get; set; } + + [JsonProperty(@"tags")] public string Tags { get; set; } public int PreviewTime { get; set; } public string AudioFile { get; set; } @@ -35,4 +41,4 @@ namespace osu.Game.Database Tags }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); } -} \ No newline at end of file +} diff --git a/osu.Game/Database/BeatmapMetrics.cs b/osu.Game/Beatmaps/BeatmapMetrics.cs similarity index 94% rename from osu.Game/Database/BeatmapMetrics.cs rename to osu.Game/Beatmaps/BeatmapMetrics.cs index 25de0f0a8d..730cf635da 100644 --- a/osu.Game/Database/BeatmapMetrics.cs +++ b/osu.Game/Beatmaps/BeatmapMetrics.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Newtonsoft.Json; -namespace osu.Game.Database +namespace osu.Game.Beatmaps { /// /// Beatmap metrics based on acculumated online data from community plays. diff --git a/osu.Game/Beatmaps/BeatmapOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapOnlineInfo.cs new file mode 100644 index 0000000000..e8f40a7e07 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapOnlineInfo.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + /// + /// Beatmap info retrieved for previewing locally without having the beatmap downloaded. + /// + public class BeatmapOnlineInfo + { + /// + /// The amount of plays this beatmap has. + /// + [JsonProperty(@"playcount")] + public int PlayCount { get; set; } + + /// + /// The amount of passes this beatmap has. + /// + [JsonProperty(@"passcount")] + public int PassCount { get; set; } + } +} diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs new file mode 100644 index 0000000000..a05362b32d --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -0,0 +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.IO; +using SQLite.Net.Attributes; +using SQLiteNetExtensions.Attributes; + +namespace osu.Game.Beatmaps +{ + public class BeatmapSetFileInfo + { + [PrimaryKey, AutoIncrement] + public int ID { get; set; } + + [ForeignKey(typeof(BeatmapSetInfo)), NotNull] + public int BeatmapSetInfoID { get; set; } + + [ForeignKey(typeof(FileInfo)), NotNull] + public int FileInfoID { get; set; } + + [OneToOne(CascadeOperations = CascadeOperation.CascadeRead)] + public FileInfo FileInfo { get; set; } + + [NotNull] + public string Filename { get; set; } + } +} \ No newline at end of file diff --git a/osu.Game/Database/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs similarity index 69% rename from osu.Game/Database/BeatmapSetInfo.cs rename to osu.Game/Beatmaps/BeatmapSetInfo.cs index 0875d3c01f..f47affcab8 100644 --- a/osu.Game/Database/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -6,7 +6,7 @@ using System.Linq; using SQLite.Net.Attributes; using SQLiteNetExtensions.Attributes; -namespace osu.Game.Database +namespace osu.Game.Beatmaps { public class BeatmapSetInfo { @@ -24,6 +24,9 @@ namespace osu.Game.Database [OneToMany(CascadeOperations = CascadeOperation.All)] public List Beatmaps { get; set; } + [Ignore] + public BeatmapSetOnlineInfo OnlineInfo { get; set; } + public double MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty); [Indexed] @@ -31,9 +34,11 @@ namespace osu.Game.Database public string Hash { get; set; } - public string Path { get; set; } + public string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(".osb"))?.Filename; - public string StoryboardFile { get; set; } + [OneToMany(CascadeOperations = CascadeOperation.All)] + public List Files { get; set; } + + public bool Protected { get; set; } } } - diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs new file mode 100644 index 0000000000..e5a1984f50 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; + +namespace osu.Game.Beatmaps +{ + /// + /// Beatmap set info retrieved for previewing locally without having the set downloaded. + /// + public class BeatmapSetOnlineInfo + { + /// + /// The different sizes of cover art for this beatmap set. + /// + [JsonProperty(@"covers")] + public BeatmapSetOnlineCovers Covers { get; set; } + + /// + /// A small sample clip of this beatmap set's song. + /// + [JsonProperty(@"previewUrl")] + public string Preview { get; set; } + + /// + /// The amount of plays this beatmap set has. + /// + [JsonProperty(@"play_count")] + public int PlayCount { get; set; } + + /// + /// The amount of people who have favourited this beatmap set. + /// + [JsonProperty(@"favourite_count")] + public int FavouriteCount { get; set; } + } + + public class BeatmapSetOnlineCovers + { + public string CoverLowRes { get; set; } + + [JsonProperty(@"cover@2x")] + public string Cover { get; set; } + + public string CardLowRes { get; set; } + + [JsonProperty(@"card@2x")] + public string Card { get; set; } + + public string ListLowRes { get; set; } + + [JsonProperty(@"list@2x")] + public string List { get; set; } + } +} diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs new file mode 100644 index 0000000000..8212712bf9 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -0,0 +1,143 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Game.Database; +using SQLite.Net; +using SQLiteNetExtensions.Extensions; + +namespace osu.Game.Beatmaps +{ + /// + /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing + /// + public class BeatmapStore : DatabaseBackedStore + { + public event Action BeatmapSetAdded; + public event Action BeatmapSetRemoved; + + /// + /// The current version of this store. Used for migrations (see ). + /// The initial version is 1. + /// + protected override int StoreVersion => 3; + + public BeatmapStore(SQLiteConnection connection) + : base(connection) + { + } + + protected override Type[] ValidTypes => new[] + { + typeof(BeatmapSetInfo), + typeof(BeatmapInfo), + typeof(BeatmapMetadata), + typeof(BeatmapDifficulty), + }; + + protected override void Prepare(bool reset = false) + { + if (reset) + { + Connection.DropTable(); + Connection.DropTable(); + Connection.DropTable(); + Connection.DropTable(); + Connection.DropTable(); + } + + Connection.CreateTable(); + Connection.CreateTable(); + Connection.CreateTable(); + Connection.CreateTable(); + Connection.CreateTable(); + } + + protected override void StartupTasks() + { + base.StartupTasks(); + cleanupPendingDeletions(); + } + + /// + /// Perform migrations between two store versions. + /// + /// The current store version. This will be zero on a fresh database initialisation. + /// The target version which we are migrating to (equal to the current ). + protected override void PerformMigration(int currentVersion, int targetVersion) + { + base.PerformMigration(currentVersion, targetVersion); + + while (currentVersion++ < targetVersion) + { + switch (currentVersion) + { + case 1: + case 2: + // cannot migrate; breaking underlying changes. + Reset(); + break; + case 3: + // Added MD5Hash column to BeatmapInfo + Connection.MigrateTable(); + break; + } + } + } + + /// + /// Add a to the database. + /// + /// The beatmap to add. + public void Add(BeatmapSetInfo beatmapSet) + { + Connection.RunInTransaction(() => + { + Connection.InsertOrReplaceWithChildren(beatmapSet, true); + }); + + BeatmapSetAdded?.Invoke(beatmapSet); + } + + /// + /// Delete a to the database. + /// + /// The beatmap to delete. + /// Whether the beatmap's was changed. + public bool Delete(BeatmapSetInfo beatmapSet) + { + if (beatmapSet.DeletePending) return false; + + beatmapSet.DeletePending = true; + Connection.Update(beatmapSet); + + BeatmapSetRemoved?.Invoke(beatmapSet); + return true; + } + + /// + /// Restore a previously deleted . + /// + /// The beatmap to restore. + /// Whether the beatmap's was changed. + public bool Undelete(BeatmapSetInfo beatmapSet) + { + if (!beatmapSet.DeletePending) return false; + + beatmapSet.DeletePending = false; + Connection.Update(beatmapSet); + + BeatmapSetAdded?.Invoke(beatmapSet); + return true; + } + + private void cleanupPendingDeletions() + { + Connection.RunInTransaction(() => + { + foreach (var b in QueryAndPopulate(b => b.DeletePending && !b.Protected)) + Connection.Delete(b, true); + }); + } + } +} diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index 0d1dc21e96..115bba5d9d 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -5,7 +5,7 @@ using System; namespace osu.Game.Beatmaps.ControlPoints { - public class ControlPoint : IComparable + public class ControlPoint : IComparable, IEquatable { /// /// The time at which the control point takes effect. @@ -13,5 +13,7 @@ namespace osu.Game.Beatmaps.ControlPoints public double Time; public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); + + public bool Equals(ControlPoint other) => Time.Equals(other?.Time); } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index acf90931ac..e7035880dd 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -91,7 +91,7 @@ namespace osu.Game.Beatmaps.ControlPoints if (time < list[0].Time) return prePoint ?? new T(); - int index = list.BinarySearch(new T() { Time = time }); + int index = list.BinarySearch(new T { Time = time }); // Check if we've found an exact match (t == time) if (index >= 0) diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 30c7884334..c622d5310c 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -10,4 +10,4 @@ namespace osu.Game.Beatmaps.ControlPoints /// public double SpeedMultiplier = 1; } -} \ No newline at end of file +} diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 6f7e38c163..c7bdbb5dd6 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -15,6 +15,6 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The beat length at this control point. /// - public double BeatLength = 500; + public double BeatLength = 1000; } } \ No newline at end of file diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs index 474d38aa1b..5071e2c1b6 100644 --- a/osu.Game/Beatmaps/DifficultyCalculator.cs +++ b/osu.Game/Beatmaps/DifficultyCalculator.cs @@ -30,11 +30,15 @@ namespace osu.Game.Beatmaps public abstract class DifficultyCalculator : DifficultyCalculator where T : HitObject { + protected readonly Beatmap Beatmap; + protected List Objects; protected DifficultyCalculator(Beatmap beatmap) { - Objects = CreateBeatmapConverter().Convert(beatmap, true).HitObjects; + Beatmap = beatmap; + + Objects = CreateBeatmapConverter().Convert(beatmap).HitObjects; foreach (var h in Objects) h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.Difficulty); diff --git a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs b/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs index 11cc6122ac..ad9a0a787b 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework; using osu.Framework.Graphics; -using osu.Game.Database; namespace osu.Game.Beatmaps.Drawables { @@ -59,10 +58,10 @@ namespace osu.Game.Beatmaps.Drawables } } - public BeatmapGroup(BeatmapSetInfo beatmapSet, BeatmapDatabase database) + public BeatmapGroup(BeatmapSetInfo beatmapSet, BeatmapManager manager) { BeatmapSet = beatmapSet; - WorkingBeatmap beatmap = database.GetWorkingBeatmap(BeatmapSet.Beatmaps.FirstOrDefault()); + WorkingBeatmap beatmap = manager.GetWorkingBeatmap(BeatmapSet.Beatmaps.FirstOrDefault()); Header = new BeatmapSetHeader(beatmap) { diff --git a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs b/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs index cb929dcca5..429ecaf416 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.UserInterface; @@ -14,6 +13,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Input; using osu.Game.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Beatmaps.Drawables { @@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Drawables GainedSelection?.Invoke(this); - background.ColourInfo = ColourInfo.GradientVertical( + background.Colour = ColourInfo.GradientVertical( new Color4(20, 43, 51, 255), new Color4(40, 86, 102, 255)); @@ -139,7 +139,7 @@ namespace osu.Game.Beatmaps.Drawables }, starCounter = new StarCounter { - Count = (float)beatmap.StarDifficulty, + CountStars = (float)beatmap.StarDifficulty, Scale = new Vector2(0.8f), } } diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs new file mode 100644 index 0000000000..df7e0905d0 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs @@ -0,0 +1,27 @@ +// 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; + +namespace osu.Game.Beatmaps.Drawables +{ + public class BeatmapSetCover : Sprite + { + private readonly BeatmapSetInfo set; + public BeatmapSetCover(BeatmapSetInfo set) + { + this.set = set; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + string resource = set.OnlineInfo.Covers.Cover; + + if (resource != null) + Texture = textures.Get(resource); + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs index 2ab5487082..a2457ba78e 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Beatmaps.Drawables { @@ -34,7 +35,7 @@ namespace osu.Game.Beatmaps.Drawables new PanelBackground(beatmap) { RelativeSizeAxes = Axes.Both, - OnLoadComplete = d => d.FadeInFromZero(400, EasingTypes.Out), + OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), } ) { @@ -92,6 +93,7 @@ namespace osu.Game.Beatmaps.Drawables { new BeatmapBackgroundSprite(working) { + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, FillMode = FillMode.Fill, @@ -117,21 +119,21 @@ namespace osu.Game.Beatmaps.Drawables new Box { RelativeSizeAxes = Axes.Both, - ColourInfo = ColourInfo.GradientHorizontal( + Colour = ColourInfo.GradientHorizontal( Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), Width = 0.05f, }, new Box { RelativeSizeAxes = Axes.Both, - ColourInfo = ColourInfo.GradientHorizontal( + Colour = ColourInfo.GradientHorizontal( new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), Width = 0.2f, }, new Box { RelativeSizeAxes = Axes.Both, - ColourInfo = ColourInfo.GradientHorizontal( + Colour = ColourInfo.GradientHorizontal( new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), Width = 0.05f, }, diff --git a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs b/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs index e91b52565a..2614baa116 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; -using osu.Game.Database; using osu.Game.Graphics; using OpenTK.Graphics; diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 9df1f0f284..42db025a40 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -3,14 +3,12 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using OpenTK; -using OpenTK.Graphics; namespace osu.Game.Beatmaps.Drawables { - public class DifficultyIcon : DifficultyColouredContainer { private readonly BeatmapInfo beatmap; @@ -24,23 +22,20 @@ namespace osu.Game.Beatmaps.Drawables [BackgroundDependencyLoader] private void load() { - Children = new[] + Children = new Drawable[] { - new TextAwesome + new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - TextSize = Size.X, + RelativeSizeAxes = Axes.Both, Colour = AccentColour, Icon = FontAwesome.fa_circle }, - new TextAwesome + new ConstrainedIconContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - TextSize = Size.X, - Colour = Color4.White, - Icon = beatmap.Ruleset.CreateInstance().Icon + RelativeSizeAxes = Axes.Both, + Icon = beatmap.Ruleset.CreateInstance().CreateIcon() } }; } diff --git a/osu.Game/Beatmaps/Drawables/Panel.cs b/osu.Game/Beatmaps/Drawables/Panel.cs index f7349d981a..d07be88392 100644 --- a/osu.Game/Beatmaps/Drawables/Panel.cs +++ b/osu.Game/Beatmaps/Drawables/Panel.cs @@ -64,9 +64,9 @@ namespace osu.Game.Beatmaps.Drawables } if (state == PanelSelectedState.Hidden) - FadeOut(300, EasingTypes.OutQuint); + this.FadeOut(300, Easing.OutQuint); else - FadeIn(250); + this.FadeIn(250); } private PanelSelectedState state = PanelSelectedState.NotSelected; @@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps.Drawables protected virtual void Selected() { nestedContainer.BorderThickness = 2.5f; - nestedContainer.EdgeEffect = new EdgeEffect + nestedContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), @@ -100,7 +100,7 @@ namespace osu.Game.Beatmaps.Drawables protected virtual void Deselected() { nestedContainer.BorderThickness = 0; - nestedContainer.EdgeEffect = new EdgeEffect + nestedContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Offset = new Vector2(1), diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs new file mode 100644 index 0000000000..479f274efb --- /dev/null +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -0,0 +1,85 @@ +// 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 osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Beatmaps +{ + internal class DummyWorkingBeatmap : WorkingBeatmap + { + private readonly OsuGameBase game; + + public DummyWorkingBeatmap(OsuGameBase game) + : base(new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "please load a beatmap!", + Title = "no beatmaps available!", + Author = "no one", + }, + BeatmapSet = new BeatmapSetInfo(), + Difficulty = new BeatmapDifficulty + { + DrainRate = 0, + CircleSize = 0, + OverallDifficulty = 0, + ApproachRate = 0, + SliderMultiplier = 0, + SliderTickRate = 0, + }, + Ruleset = new DummyRulesetInfo() + }) + { + this.game = game; + } + + protected override Beatmap GetBeatmap() => new Beatmap + { + HitObjects = new List(), + }; + + protected override Texture GetBackground() => game.Textures.Get(@"Backgrounds/bg4"); + + protected override Track GetTrack() => new TrackVirtual(); + + private class DummyRulesetInfo : RulesetInfo + { + public override Ruleset CreateInstance() => new DummyRuleset(this); + + private class DummyRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => new Mod[] { }; + + public override Mod GetAutoplayMod() => new ModAutoplay(); + + public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) + { + throw new NotImplementedException(); + } + + public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => null; + + public override ScoreProcessor CreateScoreProcessor() + { + throw new NotImplementedException(); + } + + public override string Description => "dummy"; + + public DummyRuleset(RulesetInfo rulesetInfo) + : base(rulesetInfo) + { + } + } + } + } +} diff --git a/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs index 21a3ab9842..234d65eee4 100644 --- a/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.IO; using osu.Game.Rulesets.Objects; -using osu.Game.Database; namespace osu.Game.Beatmaps.Formats { @@ -13,9 +12,16 @@ namespace osu.Game.Beatmaps.Formats { private static readonly Dictionary decoders = new Dictionary(); + static BeatmapDecoder() + { + OsuLegacyDecoder.Register(); + } + public static BeatmapDecoder GetDecoder(StreamReader stream) { - string line = stream.ReadLine()?.Trim(); + string line; + do { line = stream.ReadLine()?.Trim(); } + while (line != null && line.Length == 0); if (line == null || !decoders.ContainsKey(line)) throw new IOException(@"Unknown file format"); diff --git a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs index 4c540fa8cf..b51ea607dd 100644 --- a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs @@ -27,7 +27,9 @@ namespace osu.Game.Beatmaps.Formats AddDecoder(@"osu file format v7"); AddDecoder(@"osu file format v6"); AddDecoder(@"osu file format v5"); - // TODO: Not sure how far back to go, or differences between versions + AddDecoder(@"osu file format v4"); + AddDecoder(@"osu file format v3"); + // TODO: differences between versions } private ConvertHitObjectParser parser; @@ -222,6 +224,7 @@ namespace osu.Game.Beatmaps.Formats { while (line.IndexOf('$') >= 0) { + string origLine = line; string[] split = line.Split(','); for (int i = 0; i < split.Length; i++) { @@ -231,6 +234,7 @@ namespace osu.Game.Beatmaps.Formats } line = string.Join(",", split); + if (line == origLine) break; } } @@ -276,7 +280,7 @@ namespace osu.Game.Beatmaps.Formats double time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo); double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); - double speedMultiplier = beatLength < 0 ? -beatLength / 100.0 : 1; + double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; if (split.Length >= 3) @@ -327,6 +331,7 @@ namespace osu.Game.Beatmaps.Formats if (speedMultiplier != difficultyPoint.SpeedMultiplier) { + beatmap.ControlPointInfo.DifficultyPoints.RemoveAll(x => x.Time == time); beatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint { Time = time, @@ -456,6 +461,11 @@ namespace osu.Game.Beatmaps.Formats handleColours(beatmap, line, ref hasCustomColours); break; case Section.HitObjects: + + // If the ruleset wasn't specified, assume the osu!standard ruleset. + if (parser == null) + parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); + var obj = parser.Parse(line); if (obj != null) diff --git a/osu.Game/Beatmaps/IO/ArchiveReader.cs b/osu.Game/Beatmaps/IO/ArchiveReader.cs index 7ff5668b6f..3af2a7ea2a 100644 --- a/osu.Game/Beatmaps/IO/ArchiveReader.cs +++ b/osu.Game/Beatmaps/IO/ArchiveReader.cs @@ -5,45 +5,11 @@ using System; using System.Collections.Generic; using System.IO; using osu.Framework.IO.Stores; -using osu.Framework.Platform; namespace osu.Game.Beatmaps.IO { public abstract class ArchiveReader : IDisposable, IResourceStore { - private class Reader - { - public Func Test; - public Type Type; - } - - private static readonly List readers = new List(); - - public static ArchiveReader GetReader(Storage storage, string path) - { - foreach (var reader in readers) - { - if (reader.Test(storage, path)) - return (ArchiveReader)Activator.CreateInstance(reader.Type, storage.GetStream(path)); - } - throw new IOException(@"Unknown file format"); - } - - protected static void AddReader(Func test) where T : ArchiveReader - { - readers.Add(new Reader { Test = test, Type = typeof(T) }); - } - - /// - /// Gets a list of beatmap file names. - /// - public string[] BeatmapFilenames { get; protected set; } - - /// - /// The storyboard filename. Null if no storyboard is present. - /// - public string StoryboardFilename { get; protected set; } - /// /// Opens a stream for reading a specific file from this archive. /// @@ -51,6 +17,8 @@ namespace osu.Game.Beatmaps.IO public abstract void Dispose(); + public abstract IEnumerable Filenames { get; } + public virtual byte[] Get(string name) { using (Stream input = GetStream(name)) diff --git a/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs b/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs new file mode 100644 index 0000000000..dc38181717 --- /dev/null +++ b/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace osu.Game.Beatmaps.IO +{ + /// + /// Reads an extracted legacy beatmap from disk. + /// + public class LegacyFilesystemReader : ArchiveReader + { + private readonly string path; + + public LegacyFilesystemReader(string path) + { + this.path = path; + } + + public override Stream GetStream(string name) => File.OpenRead(Path.Combine(path, name)); + + public override void Dispose() + { + // no-op + } + + public override IEnumerable Filenames => Directory.GetFiles(path).Select(Path.GetFileName).ToArray(); + + public override Stream GetUnderlyingStream() => null; + } +} diff --git a/osu.Game/Beatmaps/IO/OszArchiveReader.cs b/osu.Game/Beatmaps/IO/OszArchiveReader.cs index eb9e740779..4e0c40d28e 100644 --- a/osu.Game/Beatmaps/IO/OszArchiveReader.cs +++ b/osu.Game/Beatmaps/IO/OszArchiveReader.cs @@ -1,25 +1,15 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using System.IO; using System.Linq; using Ionic.Zip; -using osu.Game.Beatmaps.Formats; namespace osu.Game.Beatmaps.IO { public sealed class OszArchiveReader : ArchiveReader { - public static void Register() - { - AddReader((storage, path) => - { - using (var stream = storage.GetStream(path)) - return ZipFile.IsZipFile(stream, false); - }); - OsuLegacyDecoder.Register(); - } - private readonly Stream archiveStream; private readonly ZipFile archive; @@ -27,13 +17,6 @@ namespace osu.Game.Beatmaps.IO { this.archiveStream = archiveStream; archive = ZipFile.Read(archiveStream); - - BeatmapFilenames = archive.Entries.Where(e => e.FileName.EndsWith(@".osu")).Select(e => e.FileName).ToArray(); - - if (BeatmapFilenames.Length == 0) - throw new FileNotFoundException(@"This directory contains no beatmaps"); - - StoryboardFilename = archive.Entries.Where(e => e.FileName.EndsWith(@".osb")).Select(e => e.FileName).FirstOrDefault(); } public override Stream GetStream(string name) @@ -41,7 +24,16 @@ namespace osu.Game.Beatmaps.IO ZipEntry entry = archive.Entries.SingleOrDefault(e => e.FileName == name); if (entry == null) throw new FileNotFoundException(); - return entry.OpenReader(); + + // allow seeking + MemoryStream copy = new MemoryStream(); + + using (Stream s = entry.OpenReader()) + s.CopyTo(copy); + + copy.Position = 0; + + return copy; } public override void Dispose() @@ -50,6 +42,8 @@ namespace osu.Game.Beatmaps.IO archiveStream.Dispose(); } + public override IEnumerable Filenames => archive.Entries.Select(e => e.FileName).ToArray(); + public override Stream GetUnderlyingStream() => archiveStream; } } \ No newline at end of file diff --git a/osu.Game/Database/RankStatus.cs b/osu.Game/Beatmaps/RankStatus.cs similarity index 91% rename from osu.Game/Database/RankStatus.cs rename to osu.Game/Beatmaps/RankStatus.cs index f2a7d67a40..17a4a4aa3f 100644 --- a/osu.Game/Database/RankStatus.cs +++ b/osu.Game/Beatmaps/RankStatus.cs @@ -3,7 +3,7 @@ using System.ComponentModel; -namespace osu.Game.Database +namespace osu.Game.Beatmaps { public enum RankStatus { diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 0e8d8a9546..462f94ed7c 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -4,7 +4,6 @@ using osu.Framework.Audio.Track; using osu.Framework.Configuration; using osu.Framework.Graphics.Textures; -using osu.Game.Database; using osu.Game.Rulesets.Mods; using System; using System.Collections.Generic; @@ -22,14 +21,11 @@ namespace osu.Game.Beatmaps public readonly Bindable> Mods = new Bindable>(new Mod[] { }); - public readonly bool WithStoryboard; - - protected WorkingBeatmap(BeatmapInfo beatmapInfo, bool withStoryboard = false) + protected WorkingBeatmap(BeatmapInfo beatmapInfo) { BeatmapInfo = beatmapInfo; BeatmapSetInfo = beatmapInfo.BeatmapSet; Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo.Metadata; - WithStoryboard = withStoryboard; Mods.ValueChanged += mods => applyRateAdjustments(); } @@ -56,7 +52,14 @@ namespace osu.Game.Beatmaps { lock (beatmapLock) { - return beatmap ?? (beatmap = GetBeatmap()); + if (beatmap != null) return beatmap; + + beatmap = GetBeatmap(); + + // use the database-backed info. + beatmap.BeatmapInfo = BeatmapInfo; + + return beatmap; } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 8b2a06ad0b..6b07d5c967 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -13,7 +13,6 @@ namespace osu.Game.Configuration protected override void InitialiseDefaults() { // UI/selection defaults - Set(OsuSetting.Ruleset, 0, 0, int.MaxValue); Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details); @@ -25,7 +24,6 @@ namespace osu.Game.Configuration Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2, 1); // Online settings - Set(OsuSetting.Username, string.Empty); Set(OsuSetting.Token, string.Empty); @@ -40,14 +38,12 @@ namespace osu.Game.Configuration }; // Audio - Set(OsuSetting.MenuVoice, true); Set(OsuSetting.MenuMusic, true); Set(OsuSetting.AudioOffset, 0, -500.0, 500.0); // Input - Set(OsuSetting.MenuCursorSize, 1.0, 0.5f, 2); Set(OsuSetting.GameplayCursorSize, 1.0, 0.5f, 2); Set(OsuSetting.AutoCursorSize, false); @@ -56,7 +52,6 @@ namespace osu.Game.Configuration Set(OsuSetting.MouseDisableWheel, false); // Graphics - Set(OsuSetting.ShowFpsDisplay, false); Set(OsuSetting.MenuParallax, true); @@ -65,14 +60,15 @@ namespace osu.Game.Configuration Set(OsuSetting.SnakingOutSliders, true); // Gameplay - Set(OsuSetting.DimLevel, 0.3, 0, 1); Set(OsuSetting.ShowInterface, true); Set(OsuSetting.KeyOverlay, false); - // Update + Set(OsuSetting.FloatingComments, false); + Set(OsuSetting.PlaybackSpeed, 1.0, 0.5f, 2); + // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); } @@ -90,6 +86,8 @@ namespace osu.Game.Configuration AutoCursorSize, DimLevel, KeyOverlay, + FloatingComments, + PlaybackSpeed, ShowInterface, MouseDisableButtons, MouseDisableWheel, diff --git a/osu.Game/Database/BeatmapDatabase.cs b/osu.Game/Database/BeatmapDatabase.cs deleted file mode 100644 index efd5631077..0000000000 --- a/osu.Game/Database/BeatmapDatabase.cs +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using osu.Framework.Extensions; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; -using osu.Game.Beatmaps.IO; -using osu.Game.IPC; -using osu.Game.Screens.Menu; -using SQLite.Net; -using SQLiteNetExtensions.Extensions; - -namespace osu.Game.Database -{ - public class BeatmapDatabase : Database - { - private readonly RulesetDatabase rulesets; - - public event Action BeatmapSetAdded; - public event Action BeatmapSetRemoved; - - // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) - private BeatmapIPCChannel ipc; - - public BeatmapDatabase(Storage storage, SQLiteConnection connection, RulesetDatabase rulesets, IIpcHost importHost = null) : base(storage, connection) - { - this.rulesets = rulesets; - if (importHost != null) - ipc = new BeatmapIPCChannel(importHost, this); - } - - private void deletePending() - { - foreach (var b in GetAllWithChildren(b => b.DeletePending)) - { - if (b.Hash == Intro.MENU_MUSIC_BEATMAP_HASH) - // this is a bit hacky, but will do for now. - continue; - - try - { - Storage.Delete(b.Path); - - foreach (var i in b.Beatmaps) - { - if (i.Metadata != null) Connection.Delete(i.Metadata); - if (i.Difficulty != null) Connection.Delete(i.Difficulty); - - Connection.Delete(i); - } - - if (b.Metadata != null) Connection.Delete(b.Metadata); - Connection.Delete(b); - } - catch (Exception e) - { - Logger.Error(e, $@"Could not delete beatmap {b}"); - } - } - - //this is required because sqlite migrations don't work, initially inserting nulls into this field. - //see https://github.com/praeclarum/sqlite-net/issues/326 - Connection.Query("UPDATE BeatmapSetInfo SET DeletePending = 0 WHERE DeletePending IS NULL"); - } - - protected override void Prepare(bool reset = false) - { - Connection.CreateTable(); - Connection.CreateTable(); - Connection.CreateTable(); - Connection.CreateTable(); - - if (reset) - { - Storage.DeleteDatabase(@"beatmaps"); - - foreach (var setInfo in Query()) - { - if (Storage.Exists(setInfo.Path)) - Storage.Delete(setInfo.Path); - } - - Connection.DeleteAll(); - Connection.DeleteAll(); - Connection.DeleteAll(); - Connection.DeleteAll(); - } - - deletePending(); - } - - protected override Type[] ValidTypes => new[] { - typeof(BeatmapSetInfo), - typeof(BeatmapInfo), - typeof(BeatmapMetadata), - typeof(BeatmapDifficulty), - }; - - public void Import(string path) - { - try - { - Import(ArchiveReader.GetReader(Storage, path)); - - // We may or may not want to delete the file depending on where it is stored. - // e.g. reconstructing/repairing database with beatmaps from default storage. - // Also, not always a single file, i.e. for LegacyFilesystemReader - // TODO: Add a check to prevent files from storage to be deleted. - try - { - File.Delete(path); - } - catch (Exception e) - { - Logger.Error(e, $@"Could not delete file at {path}"); - } - } - catch (Exception e) - { - e = e.InnerException ?? e; - Logger.Error(e, @"Could not import beatmap set"); - } - } - - public void Import(ArchiveReader archiveReader) - { - BeatmapSetInfo set = getBeatmapSet(archiveReader); - - //If we have an ID then we already exist in the database. - if (set.ID == 0) - Import(new[] { set }); - } - - /// - /// Import multiple from . - /// - /// Multiple locations on disk - public void Import(params string[] paths) - { - foreach (string p in paths) - Import(p); - } - - /// - /// Duplicates content from to storage and returns a representing . - /// - /// Content location - /// - private BeatmapSetInfo getBeatmapSet(string path) => getBeatmapSet(ArchiveReader.GetReader(Storage, path)); - - private BeatmapSetInfo getBeatmapSet(ArchiveReader archiveReader) - { - BeatmapMetadata metadata; - - using (var stream = new StreamReader(archiveReader.GetStream(archiveReader.BeatmapFilenames[0]))) - metadata = BeatmapDecoder.GetDecoder(stream).Decode(stream).Metadata; - - string hash; - string path; - - using (var input = archiveReader.GetUnderlyingStream()) - { - hash = input.GetMd5Hash(); - input.Seek(0, SeekOrigin.Begin); - path = Path.Combine(@"beatmaps", hash.Remove(1), hash.Remove(2), hash); - if (!Storage.Exists(path)) - using (var output = Storage.GetStream(path, FileAccess.Write)) - input.CopyTo(output); - } - - var existing = Connection.Table().FirstOrDefault(b => b.Hash == hash); - - if (existing != null) - { - if (existing.DeletePending) - { - existing.DeletePending = false; - Update(existing, false); - BeatmapSetAdded?.Invoke(existing); - } - - return existing; - } - - var beatmapSet = new BeatmapSetInfo - { - OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, - Beatmaps = new List(), - Path = path, - Hash = hash, - Metadata = metadata - }; - - using (var archive = ArchiveReader.GetReader(Storage, path)) - { - string[] mapNames = archive.BeatmapFilenames; - foreach (var name in mapNames) - using (var raw = archive.GetStream(name)) - using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit - using (var sr = new StreamReader(ms)) - { - raw.CopyTo(ms); - ms.Position = 0; - - var decoder = BeatmapDecoder.GetDecoder(sr); - Beatmap beatmap = decoder.Decode(sr); - - beatmap.BeatmapInfo.Path = name; - beatmap.BeatmapInfo.Hash = ms.GetMd5Hash(); - - // TODO: Diff beatmap metadata with set metadata and leave it here if necessary - beatmap.BeatmapInfo.Metadata = null; - - // TODO: this should be done in a better place once we actually need to dynamically update it. - beatmap.BeatmapInfo.Ruleset = rulesets.Query().FirstOrDefault(r => r.ID == beatmap.BeatmapInfo.RulesetID); - beatmap.BeatmapInfo.StarDifficulty = rulesets.Query().FirstOrDefault(r => r.ID == beatmap.BeatmapInfo.RulesetID)?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0; - - beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); - } - beatmapSet.StoryboardFile = archive.StoryboardFilename; - } - - return beatmapSet; - } - - public void Import(IEnumerable beatmapSets) - { - lock (Connection) - { - Connection.BeginTransaction(); - - foreach (var s in beatmapSets) - { - Connection.InsertOrReplaceWithChildren(s, true); - BeatmapSetAdded?.Invoke(s); - } - - Connection.Commit(); - } - } - - public void Delete(BeatmapSetInfo beatmapSet) - { - beatmapSet.DeletePending = true; - Update(beatmapSet, false); - - BeatmapSetRemoved?.Invoke(beatmapSet); - } - - public ArchiveReader GetReader(BeatmapSetInfo beatmapSet) - { - if (string.IsNullOrEmpty(beatmapSet.Path)) - return null; - - return ArchiveReader.GetReader(Storage, beatmapSet.Path); - } - - public BeatmapSetInfo GetBeatmapSet(int id) - { - return Query().FirstOrDefault(s => s.OnlineBeatmapSetID == id); - } - - public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null, bool withStoryboard = false) - { - if (beatmapInfo.BeatmapSet == null || beatmapInfo.Ruleset == null) - beatmapInfo = GetChildren(beatmapInfo, true); - - if (beatmapInfo.BeatmapSet == null) - throw new InvalidOperationException($@"Beatmap set {beatmapInfo.BeatmapSetInfoID} is not in the local database."); - - if (beatmapInfo.Metadata == null) - beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; - - WorkingBeatmap working = new DatabaseWorkingBeatmap(this, beatmapInfo, withStoryboard); - - previous?.TransferTo(working); - - return working; - } - - public bool Exists(BeatmapSetInfo beatmapSet) => Storage.Exists(beatmapSet.Path); - } -} diff --git a/osu.Game/Database/BeatmapOnlineInfo.cs b/osu.Game/Database/BeatmapOnlineInfo.cs deleted file mode 100644 index bcb2ef16ac..0000000000 --- a/osu.Game/Database/BeatmapOnlineInfo.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace osu.Game.Database -{ - /// - /// Beatmap info retrieved for previewing locally without having the beatmap downloaded. - /// - public class BeatmapOnlineInfo - { - /// - /// The different sizes of cover art for this beatmap: cover, cover@2x, card, card@2x, list, list@2x. - /// - [JsonProperty(@"covers")] - public IEnumerable Covers { get; set; } - - /// - /// A small sample clip of this beatmap's song. - /// - [JsonProperty(@"previewUrl")] - public string Preview { get; set; } - - /// - /// The amount of plays this beatmap has. - /// - [JsonProperty(@"play_count")] - public int PlayCount { get; set; } - - /// - /// The amount of people who have favourited this map. - /// - [JsonProperty(@"favourite_count")] - public int FavouriteCount { get; set; } - } -} diff --git a/osu.Game/Database/Database.cs b/osu.Game/Database/Database.cs deleted file mode 100644 index a55c0f570b..0000000000 --- a/osu.Game/Database/Database.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using osu.Framework.Logging; -using osu.Framework.Platform; -using SQLite.Net; -using SQLiteNetExtensions.Extensions; - -namespace osu.Game.Database -{ - public abstract class Database - { - protected SQLiteConnection Connection { get; } - protected Storage Storage { get; } - - protected Database(Storage storage, SQLiteConnection connection) - { - Storage = storage; - Connection = connection; - - try - { - Prepare(); - } - catch (Exception e) - { - Logger.Error(e, $@"Failed to initialise the {GetType()}! Trying again with a clean database..."); - Prepare(true); - } - } - - /// - /// Prepare this database for use. - /// - protected abstract void Prepare(bool reset = false); - - /// - /// Reset this database to a default state. Undo all changes to database and storage backings. - /// - public void Reset() => Prepare(true); - - public TableQuery Query() where T : class - { - return Connection.Table(); - } - - /// - /// This is expensive. Use with caution. - /// - public List GetAllWithChildren(Expression> filter = null, bool recursive = true) - where T : class - { - return Connection.GetAllWithChildren(filter, recursive); - } - - public T GetChildren(T item, bool recursive = false) - { - if (item == null) return default(T); - - Connection.GetChildren(item, recursive); - return item; - } - - protected abstract Type[] ValidTypes { get; } - - public void Update(T record, bool cascade = true) where T : class - { - if (ValidTypes.All(t => t != typeof(T))) - throw new ArgumentException("Must be a type managed by BeatmapDatabase", nameof(T)); - if (cascade) - Connection.UpdateWithChildren(record); - else - Connection.Update(record); - } - } -} \ No newline at end of file diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs new file mode 100644 index 0000000000..d8e2e35bd7 --- /dev/null +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -0,0 +1,131 @@ +// 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 System.Linq.Expressions; +using osu.Framework.Logging; +using osu.Framework.Platform; +using SQLite.Net; +using SQLiteNetExtensions.Extensions; + +namespace osu.Game.Database +{ + public abstract class DatabaseBackedStore + { + protected readonly Storage Storage; + protected readonly SQLiteConnection Connection; + + protected virtual int StoreVersion => 1; + + protected DatabaseBackedStore(SQLiteConnection connection, Storage storage = null) + { + Storage = storage; + Connection = connection; + + try + { + Prepare(); + } + catch (Exception e) + { + Logger.Error(e, $@"Failed to initialise the {GetType()}! Trying again with a clean database..."); + Prepare(true); + } + + checkMigrations(); + } + + private void checkMigrations() + { + var storeName = GetType().Name; + + var reportedVersion = Connection.Table().Where(s => s.StoreName == storeName).FirstOrDefault() ?? new StoreVersion + { + StoreName = storeName, + Version = 0 + }; + + if (reportedVersion.Version != StoreVersion) + PerformMigration(reportedVersion.Version, reportedVersion.Version = StoreVersion); + + Connection.InsertOrReplace(reportedVersion); + + StartupTasks(); + } + + /// + /// Called when the database version of this store doesn't match the local version. + /// Any manual migration operations should be performed in this. + /// + /// The current store version. This will be zero on a fresh database initialisation. + /// The target version which we are migrating to (equal to the current ). + protected virtual void PerformMigration(int currentVersion, int targetVersion) + { + } + + /// + /// Perform any common startup tasks. Runs after and . + /// + protected virtual void StartupTasks() + { + + } + + /// + /// Prepare this database for use. Tables should be created here. + /// + protected abstract void Prepare(bool reset = false); + + /// + /// Reset this database to a default state. Undo all changes to database and storage backings. + /// + public void Reset() => Prepare(true); + + + public TableQuery Query(Expression> filter = null) where T : class + { + checkType(typeof(T)); + + var query = Connection.Table(); + + if (filter != null) + query = query.Where(filter); + + return query; + } + + /// + /// Query and populate results. + /// + /// An filter to refine results. + /// + public List QueryAndPopulate(Expression> filter) + where T : class + { + checkType(typeof(T)); + + return Connection.GetAllWithChildren(filter, true); + } + + /// + /// Populate a database-backed item. + /// + /// + /// Whether population should recurse beyond a single level. + public void Populate(T item, bool recursive = true) + { + checkType(item.GetType()); + Connection.GetChildren(item, recursive); + } + + private void checkType(Type type) + { + if (!ValidTypes.Contains(type)) + throw new InvalidOperationException($"The requested operation specified a type of {type}, which is invalid for this {nameof(DatabaseBackedStore)}."); + } + + protected abstract Type[] ValidTypes { get; } + } +} diff --git a/osu.Game/Database/DatabaseWorkingBeatmap.cs b/osu.Game/Database/DatabaseWorkingBeatmap.cs deleted file mode 100644 index c56d6cea51..0000000000 --- a/osu.Game/Database/DatabaseWorkingBeatmap.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.IO; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Textures; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; -using osu.Game.Beatmaps.IO; - -namespace osu.Game.Database -{ - internal class DatabaseWorkingBeatmap : WorkingBeatmap - { - private readonly BeatmapDatabase database; - - public DatabaseWorkingBeatmap(BeatmapDatabase database, BeatmapInfo beatmapInfo, bool withStoryboard = false) - : base(beatmapInfo, withStoryboard) - { - this.database = database; - } - - private ArchiveReader getReader() => database?.GetReader(BeatmapSetInfo); - - protected override Beatmap GetBeatmap() - { - try - { - Beatmap beatmap; - - using (var reader = getReader()) - { - BeatmapDecoder decoder; - using (var stream = new StreamReader(reader.GetStream(BeatmapInfo.Path))) - { - decoder = BeatmapDecoder.GetDecoder(stream); - beatmap = decoder.Decode(stream); - } - - if (beatmap == null || !WithStoryboard || BeatmapSetInfo.StoryboardFile == null) - return beatmap; - - using (var stream = new StreamReader(reader.GetStream(BeatmapSetInfo.StoryboardFile))) - decoder.Decode(stream, beatmap); - } - - return beatmap; - } - catch { return null; } - } - - protected override Texture GetBackground() - { - if (Metadata?.BackgroundFile == null) - return null; - - try - { - using (var reader = getReader()) - return new TextureStore(new RawTextureLoaderStore(reader), false).Get(Metadata.BackgroundFile); - } - catch { return null; } - } - - protected override Track GetTrack() - { - try - { - var trackData = getReader()?.GetStream(Metadata.AudioFile); - return trackData == null ? null : new TrackBass(trackData); - } - catch { return null; } - } - } -} \ No newline at end of file diff --git a/osu.Game/Database/OnlineWorkingBeatmap.cs b/osu.Game/Database/OnlineWorkingBeatmap.cs deleted file mode 100644 index 1465c59434..0000000000 --- a/osu.Game/Database/OnlineWorkingBeatmap.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Linq; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Textures; -using osu.Game.Beatmaps; - -namespace osu.Game.Database -{ - internal class OnlineWorkingBeatmap : WorkingBeatmap - { - private readonly TextureStore textures; - private readonly TrackManager tracks; - - public OnlineWorkingBeatmap(BeatmapInfo beatmapInfo, TextureStore textures, TrackManager tracks) : base(beatmapInfo) - { - this.textures = textures; - this.tracks = tracks; - } - - protected override Beatmap GetBeatmap() - { - return new Beatmap(); - } - - protected override Texture GetBackground() - { - return textures.Get(BeatmapInfo.OnlineInfo.Covers.FirstOrDefault()); - } - - protected override Track GetTrack() - { - return tracks.Get(BeatmapInfo.OnlineInfo.Preview); - } - } -} diff --git a/osu.Game/Database/StoreVersion.cs b/osu.Game/Database/StoreVersion.cs new file mode 100644 index 0000000000..00314875a6 --- /dev/null +++ b/osu.Game/Database/StoreVersion.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using SQLite.Net.Attributes; + +namespace osu.Game.Database +{ + public class StoreVersion + { + [PrimaryKey] + public string StoreName { get; set; } + + public int Version { get; set; } + } +} diff --git a/osu.Game/Graphics/Backgrounds/Background.cs b/osu.Game/Graphics/Backgrounds/Background.cs index 7c47635276..8eb2ddc0ab 100644 --- a/osu.Game/Graphics/Backgrounds/Background.cs +++ b/osu.Game/Graphics/Backgrounds/Background.cs @@ -26,6 +26,7 @@ namespace osu.Game.Graphics.Backgrounds Add(Sprite = new Sprite { + RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = Color4.DarkGray, diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 7a2345a80c..08745ce6ba 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -2,12 +2,10 @@ // 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.MathUtils; using OpenTK; using OpenTK.Graphics; using System; -using osu.Framework.Graphics.OpenGL; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Textures; using OpenTK.Graphics.ES30; @@ -16,6 +14,7 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Allocation; using System.Collections.Generic; using osu.Framework.Graphics.Batches; +using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Lists; namespace osu.Game.Graphics.Backgrounds @@ -56,7 +55,7 @@ namespace osu.Game.Graphics.Backgrounds /// /// Whether we should drop-off alpha values of triangles more quickly to improve /// the visual appearance of fading. This defaults to on as it is generally more - /// aesthetically pleasing, but should be turned off in s. + /// aesthetically pleasing, but should be turned off in buffered containers. /// public bool HideAlphaDiscrepancies = true; diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index c0defceac0..123ef0662d 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -23,12 +23,24 @@ namespace osu.Game.Graphics.Containers /// protected double EarlyActivationMilliseconds; + /// + /// The time in milliseconds until the next beat. + /// + public double TimeUntilNextBeat { get; private set; } + + /// + /// The time in milliseconds since the last beat + /// + public double TimeSinceLastBeat { get; private set; } + protected override void Update() { - if (Beatmap.Value?.Track == null) + var track = Beatmap.Value.Track; + + if (track == null) return; - double currentTrackTime = Beatmap.Value.Track.CurrentTime + EarlyActivationMilliseconds; + double currentTrackTime = track.Length > 0 ? track.CurrentTime + EarlyActivationMilliseconds : Clock.CurrentTime; TimingControlPoint timingPoint = Beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); EffectControlPoint effectPoint = Beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); @@ -42,12 +54,16 @@ namespace osu.Game.Graphics.Containers if (currentTrackTime < timingPoint.Time) beatIndex--; - if (timingPoint == lastTimingPoint && beatIndex == lastBeat) + TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % timingPoint.BeatLength; + if (TimeUntilNextBeat < 0) + TimeUntilNextBeat += timingPoint.BeatLength; + + TimeSinceLastBeat = timingPoint.BeatLength - TimeUntilNextBeat; + + if (timingPoint.Equals(lastTimingPoint) && beatIndex == lastBeat) return; - double offsetFromBeat = (timingPoint.Time - currentTrackTime) % timingPoint.BeatLength; - - using (BeginDelayedSequence(offsetFromBeat, true)) + using (BeginDelayedSequence(-TimeSinceLastBeat, true)) OnNewBeat(beatIndex, timingPoint, effectPoint, Beatmap.Value.Track.CurrentAmplitudes); lastBeat = beatIndex; diff --git a/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs new file mode 100644 index 0000000000..dd2a265a0f --- /dev/null +++ b/osu.Game/Graphics/Containers/ConstrainedIconContainer.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using OpenTK; + +namespace osu.Game.Graphics.Containers +{ + /// + /// Display an icon that is forced to scale to the size of this container. + /// + public class ConstrainedIconContainer : CompositeDrawable + { + public Drawable Icon + { + get + { + return InternalChild; + } + + set + { + InternalChild = value; + } + } + + /// + /// Determines an edge effect of this . + /// Edge effects are e.g. glow or a shadow. + /// Only has an effect when is true. + /// + public new EdgeEffectParameters EdgeEffect + { + get { return base.EdgeEffect; } + set { base.EdgeEffect = value; } + } + + protected override void Update() + { + base.Update(); + if (InternalChildren.Count > 0 && InternalChild.DrawSize.X > 0) + { + // We're modifying scale here for a few reasons + // - Guarantees correctness if BorderWidth is being used + // - If we were to use RelativeSize/FillMode, we'd need to set the Icon's RelativeSizeAxes directly. + // We can't do this because we would need access to AutoSizeAxes to set it to none. + // Other issues come up along the way too, so it's not a good solution. + var fitScale = Math.Min(DrawSize.X / InternalChild.DrawSize.X, DrawSize.Y / InternalChild.DrawSize.Y); + InternalChild.Scale = new Vector2(fitScale); + InternalChild.Anchor = Anchor.Centre; + InternalChild.Origin = Anchor.Centre; + } + } + + public ConstrainedIconContainer() + { + Masking = true; + } + } +} diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs new file mode 100644 index 0000000000..11c049ed3e --- /dev/null +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -0,0 +1,35 @@ +// 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.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; + +namespace osu.Game.Graphics.Containers +{ + public class OsuClickableContainer : ClickableContainer + { + protected SampleChannel SampleClick, SampleHover; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + SampleHover = audio.Sample.Get(@"UI/generic-hover"); + SampleClick = audio.Sample.Get(@"UI/generic-click"); + } + + protected override bool OnHover(InputState state) + { + SampleHover?.Play(); + return base.OnHover(state); + } + + protected override bool OnClick(InputState state) + { + SampleClick?.Play(); + return base.OnClick(state); + } + } +} diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs new file mode 100644 index 0000000000..0713fa1a52 --- /dev/null +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Graphics.Containers +{ + public class OsuFocusedOverlayContainer : FocusedOverlayContainer + { + private SampleChannel samplePopIn; + private SampleChannel samplePopOut; + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + samplePopIn = audio.Sample.Get(@"UI/melodic-5"); + samplePopOut = audio.Sample.Get(@"UI/melodic-4"); + + StateChanged += OsuFocusedOverlayContainer_StateChanged; + } + + private void OsuFocusedOverlayContainer_StateChanged(VisibilityContainer arg1, Visibility arg2) + { + switch (arg2) + { + case Visibility.Visible: + samplePopIn?.Play(); + break; + case Visibility.Hidden: + samplePopOut?.Play(); + break; + } + } + } +} diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs new file mode 100644 index 0000000000..e395f1b7bd --- /dev/null +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using OpenTK.Input; + +namespace osu.Game.Graphics.Containers +{ + internal class OsuScrollContainer : ScrollContainer + { + /// + /// Allows controlling the scroll bar from any position in the container using the right mouse button. + /// Uses the value of to smoothly scroll to the dragged location. + /// + public bool RightMouseScrollbar = false; + + /// + /// Controls the rate with which the target position is approached when performing a relative drag. Default is 0.02. + /// + public double DistanceDecayOnRightMouseScrollbar = 0.02; + + private bool shouldPerformRightMouseScroll(InputState state) => RightMouseScrollbar && state.Mouse.IsPressed(MouseButton.Right); + + private void scrollToRelative(float value) => ScrollTo(Clamp((value - Scrollbar.DrawSize[ScrollDim] / 2) / Scrollbar.Size[ScrollDim]), true, DistanceDecayOnRightMouseScrollbar); + + private bool mouseScrollBarDragging; + + protected override bool IsDragging => base.IsDragging || mouseScrollBarDragging; + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (shouldPerformRightMouseScroll(state)) + { + scrollToRelative(state.Mouse.Position[ScrollDim]); + return true; + } + + return base.OnMouseDown(state, args); + } + + protected override bool OnDrag(InputState state) + { + if (mouseScrollBarDragging) + { + scrollToRelative(state.Mouse.Position[ScrollDim]); + return true; + } + + return base.OnDrag(state); + } + + protected override bool OnDragStart(InputState state) + { + if (shouldPerformRightMouseScroll(state)) + { + mouseScrollBarDragging = true; + return true; + } + + return base.OnDragStart(state); + } + + protected override bool OnDragEnd(InputState state) + { + if (mouseScrollBarDragging) + { + mouseScrollBarDragging = false; + return true; + } + + return base.OnDragEnd(state); + } + } +} diff --git a/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs b/osu.Game/Graphics/Containers/OsuTextFlowContainer.cs new file mode 100644 index 0000000000..143f38ced4 --- /dev/null +++ b/osu.Game/Graphics/Containers/OsuTextFlowContainer.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 + +using System; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics.Containers +{ + public class OsuTextFlowContainer : TextFlowContainer + { + public OsuTextFlowContainer(Action defaultCreationParameters = null) : base(defaultCreationParameters) + { + } + + protected override SpriteText CreateSpriteText() => new OsuSpriteText(); + + public void AddIcon(FontAwesome icon, Action creationParameters = null) => AddText(((char)icon).ToString(), creationParameters); + } +} diff --git a/osu.Game/Graphics/Containers/ParallaxContainer.cs b/osu.Game/Graphics/Containers/ParallaxContainer.cs index 2d5952a3ce..3e0ed4b059 100644 --- a/osu.Game/Graphics/Containers/ParallaxContainer.cs +++ b/osu.Game/Graphics/Containers/ParallaxContainer.cs @@ -19,8 +19,6 @@ namespace osu.Game.Graphics.Containers public ParallaxContainer() { - AlwaysReceiveInput = true; - RelativeSizeAxes = Axes.Both; AddInternal(content = new Container { @@ -36,20 +34,25 @@ namespace osu.Game.Graphics.Containers protected override Container Content => content; [BackgroundDependencyLoader] - private void load(UserInputManager input, OsuConfigManager config) + private void load(OsuConfigManager config) { - this.input = input; parallaxEnabled = config.GetBindable(OsuSetting.MenuParallax); parallaxEnabled.ValueChanged += delegate { if (!parallaxEnabled) { - content.MoveTo(Vector2.Zero, firstUpdate ? 0 : 1000, EasingTypes.OutQuint); + content.MoveTo(Vector2.Zero, firstUpdate ? 0 : 1000, Easing.OutQuint); content.Scale = new Vector2(1 + ParallaxAmount); } }; } + protected override void LoadComplete() + { + base.LoadComplete(); + input = GetContainingInputManager(); + } + private bool firstUpdate = true; protected override void Update() @@ -59,7 +62,7 @@ namespace osu.Game.Graphics.Containers if (parallaxEnabled) { Vector2 offset = input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.NativeState.Position) - DrawSize / 2; - content.MoveTo(offset * ParallaxAmount, firstUpdate ? 0 : 1000, EasingTypes.OutQuint); + content.MoveTo(offset * ParallaxAmount, firstUpdate ? 0 : 1000, Easing.OutQuint); content.Scale = new Vector2(1 + ParallaxAmount); } diff --git a/osu.Game/Graphics/Containers/ReverseDepthFillFlowContainer.cs b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs similarity index 67% rename from osu.Game/Graphics/Containers/ReverseDepthFillFlowContainer.cs rename to osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs index 89b4a90010..781e149078 100644 --- a/osu.Game/Graphics/Containers/ReverseDepthFillFlowContainer.cs +++ b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs @@ -8,9 +8,10 @@ using osu.Framework.Graphics.Containers; namespace osu.Game.Graphics.Containers { - public class ReverseDepthFillFlowContainer : FillFlowContainer where T : Drawable + public class ReverseChildIDFillFlowContainer : FillFlowContainer where T : Drawable { - protected override IComparer DepthComparer => new ReverseCreationOrderDepthComparer(); + protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); + protected override IEnumerable FlowingChildren => base.FlowingChildren.Reverse(); } } diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 5fdb5e869e..6e5c3c8183 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -2,7 +2,6 @@ // 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.Configuration; using osu.Framework.Graphics; @@ -13,11 +12,15 @@ namespace osu.Game.Graphics.Containers /// /// A container that can scroll to each section inside it. /// - public class SectionsContainer : Container + public class SectionsContainer : Container + where T : Drawable { - private Drawable expandableHeader, fixedHeader, footer; - public readonly ScrollContainer ScrollContainer; - private readonly Container sectionsContainer; + private Drawable expandableHeader, fixedHeader, footer, headerBackground; + private readonly ScrollContainer scrollContainer; + private readonly Container headerBackgroundContainer; + private readonly FlowContainer scrollContentContainer; + + protected override Container Content => scrollContentContainer; public Drawable ExpandableHeader { @@ -26,12 +29,11 @@ namespace osu.Game.Graphics.Containers { if (value == expandableHeader) return; - if (expandableHeader != null) - Remove(expandableHeader); + expandableHeader?.Expire(); expandableHeader = value; if (value == null) return; - Add(expandableHeader); + AddInternal(expandableHeader); lastKnownScroll = float.NaN; } } @@ -43,12 +45,11 @@ namespace osu.Game.Graphics.Containers { if (value == fixedHeader) return; - if (fixedHeader != null) - Remove(fixedHeader); + fixedHeader?.Expire(); fixedHeader = value; if (value == null) return; - Add(fixedHeader); + AddInternal(fixedHeader); lastKnownScroll = float.NaN; } } @@ -61,68 +62,83 @@ namespace osu.Game.Graphics.Containers if (value == footer) return; if (footer != null) - ScrollContainer.Remove(footer); + scrollContainer.Remove(footer); footer = value; if (value == null) return; footer.Anchor |= Anchor.y2; footer.Origin |= Anchor.y2; - ScrollContainer.Add(footer); + scrollContainer.Add(footer); lastKnownScroll = float.NaN; } } - public Bindable SelectedSection { get; } = new Bindable(); - - protected virtual Container CreateScrollContentContainer() - => new FillFlowContainer - { - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both - }; - - private List sections = new List(); - public IEnumerable Sections + public Drawable HeaderBackground { - get { return sections; } + get { return headerBackground; } set { - foreach (var section in sections) - sectionsContainer.Remove(section); + if (value == headerBackground) return; - sections = value.ToList(); - if (sections.Count == 0) return; + headerBackgroundContainer.Clear(); + headerBackground = value; + if (value == null) return; + + headerBackgroundContainer.Add(headerBackground); - sectionsContainer.Add(sections); - SelectedSection.Value = sections[0]; lastKnownScroll = float.NaN; } } + public Bindable SelectedSection { get; } = new Bindable(); + + protected virtual FlowContainer CreateScrollContentContainer() + => new FillFlowContainer + { + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }; + + public override void Add(T drawable) + { + base.Add(drawable); + lastKnownScroll = float.NaN; + headerHeight = float.NaN; + footerHeight = float.NaN; + } + private float headerHeight, footerHeight; private readonly MarginPadding originalSectionsMargin; private void updateSectionsMargin() { - if (sections.Count == 0) return; + if (!Children.Any()) return; var newMargin = originalSectionsMargin; newMargin.Top += headerHeight; newMargin.Bottom += footerHeight; - sectionsContainer.Margin = newMargin; + scrollContentContainer.Margin = newMargin; } public SectionsContainer() { - Add(ScrollContainer = new ScrollContainer() + AddInternal(scrollContainer = new ScrollContainer { RelativeSizeAxes = Axes.Both, - Masking = false, - Children = new Drawable[] { sectionsContainer = CreateScrollContentContainer() } + Masking = true, + ScrollbarVisible = false, + Children = new Drawable[] { scrollContentContainer = CreateScrollContentContainer() } }); - originalSectionsMargin = sectionsContainer.Margin; + AddInternal(headerBackgroundContainer = new Container + { + RelativeSizeAxes = Axes.X + }); + originalSectionsMargin = scrollContentContainer.Margin; } + public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); + private float lastKnownScroll; protected override void UpdateAfterChildren() { @@ -137,25 +153,30 @@ namespace osu.Game.Graphics.Containers updateSectionsMargin(); } - float currentScroll = Math.Max(0, ScrollContainer.Current); + float currentScroll = scrollContainer.Current; + if (currentScroll != lastKnownScroll) { lastKnownScroll = currentScroll; - if (expandableHeader != null && fixedHeader != null) + if (ExpandableHeader != null && FixedHeader != null) { - float offset = Math.Min(expandableHeader.LayoutSize.Y, currentScroll); + float offset = Math.Min(ExpandableHeader.LayoutSize.Y, currentScroll); - expandableHeader.Y = -offset; - fixedHeader.Y = -offset + expandableHeader.LayoutSize.Y; + ExpandableHeader.Y = -offset; + FixedHeader.Y = -offset + ExpandableHeader.LayoutSize.Y; } - Drawable bestMatch = null; - float minDiff = float.MaxValue; + headerBackgroundContainer.Height = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); + headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0; - foreach (var section in sections) + T bestMatch = null; + float minDiff = float.MaxValue; + float scrollOffset = FixedHeader?.LayoutSize.Y ?? 0; + + foreach (var section in Children) { - float diff = Math.Abs(ScrollContainer.GetChildPosInContent(section) - currentScroll); + float diff = Math.Abs(scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset); if (diff < minDiff) { minDiff = diff; diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 82ae424ab0..9658cfdb09 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -34,7 +34,7 @@ namespace osu.Game.Graphics.Cursor if (diff > 180) diff -= 360; degrees = ActiveCursor.Rotation + diff; - ActiveCursor.RotateTo(degrees, 600, EasingTypes.OutQuint); + ActiveCursor.RotateTo(degrees, 600, Easing.OutQuint); } return base.OnMouseMove(state); @@ -49,10 +49,10 @@ namespace osu.Game.Graphics.Cursor protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { ActiveCursor.Scale = new Vector2(1); - ActiveCursor.ScaleTo(0.90f, 800, EasingTypes.OutQuint); + ActiveCursor.ScaleTo(0.90f, 800, Easing.OutQuint); ((Cursor)ActiveCursor).AdditiveLayer.Alpha = 0; - ((Cursor)ActiveCursor).AdditiveLayer.FadeInFromZero(800, EasingTypes.OutQuint); + ((Cursor)ActiveCursor).AdditiveLayer.FadeInFromZero(800, Easing.OutQuint); return base.OnMouseDown(state, args); } @@ -62,9 +62,9 @@ namespace osu.Game.Graphics.Cursor { dragging = false; - ((Cursor)ActiveCursor).AdditiveLayer.FadeOut(500, EasingTypes.OutQuint); - ActiveCursor.RotateTo(0, 600 * (1 + Math.Abs(ActiveCursor.Rotation / 720)), EasingTypes.OutElasticHalf); - ActiveCursor.ScaleTo(1, 500, EasingTypes.OutElastic); + ((Cursor)ActiveCursor).AdditiveLayer.FadeOut(500, Easing.OutQuint); + ActiveCursor.RotateTo(0, 600 * (1 + Math.Abs(ActiveCursor.Rotation / 720)), Easing.OutElasticHalf); + ActiveCursor.ScaleTo(1, 500, Easing.OutElastic); } return base.OnMouseUp(state, args); @@ -72,21 +72,21 @@ namespace osu.Game.Graphics.Cursor protected override bool OnClick(InputState state) { - ((Cursor)ActiveCursor).AdditiveLayer.FadeOutFromOne(500, EasingTypes.OutQuint); + ((Cursor)ActiveCursor).AdditiveLayer.FadeOutFromOne(500, Easing.OutQuint); return base.OnClick(state); } protected override void PopIn() { - ActiveCursor.FadeTo(1, 250, EasingTypes.OutQuint); - ActiveCursor.ScaleTo(1, 400, EasingTypes.OutQuint); + ActiveCursor.FadeTo(1, 250, Easing.OutQuint); + ActiveCursor.ScaleTo(1, 400, Easing.OutQuint); } protected override void PopOut() { - ActiveCursor.FadeTo(0, 900, EasingTypes.OutQuint); - ActiveCursor.ScaleTo(0, 500, EasingTypes.In); + ActiveCursor.FadeTo(0, 900, Easing.OutQuint); + ActiveCursor.ScaleTo(0, 500, Easing.In); } public class Cursor : Container diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs new file mode 100644 index 0000000000..9162fd6893 --- /dev/null +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Graphics.Cursor +{ + public class OsuContextMenuContainer : ContextMenuContainer + { + protected override ContextMenu CreateContextMenu() => new OsuContextMenu(); + } +} \ No newline at end of file diff --git a/osu.Game/Graphics/Cursor/OsuCursorContainer.cs b/osu.Game/Graphics/Cursor/OsuCursorContainer.cs deleted file mode 100644 index 8b71182263..0000000000 --- a/osu.Game/Graphics/Cursor/OsuCursorContainer.cs +++ /dev/null @@ -1,129 +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 OpenTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Configuration; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Input; -using osu.Game.Configuration; -using System; - -namespace osu.Game.Graphics.Cursor -{ - public class OsuCursorContainer : CursorContainer - { - protected override Drawable CreateCursor() => new OsuCursor(); - - public OsuCursorContainer() - { - Add(new CursorTrail { Depth = 1 }); - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - ActiveCursor.Scale = new Vector2(1); - ActiveCursor.ScaleTo(1.2f, 100, EasingTypes.OutQuad); - return base.OnMouseDown(state, args); - } - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) - { - if (!state.Mouse.HasMainButtonPressed) - ActiveCursor.ScaleTo(1, 200, EasingTypes.OutQuad); - return base.OnMouseUp(state, args); - } - - public class OsuCursor : Container - { - private Container cursorContainer; - private Bindable cursorScale; - - public OsuCursor() - { - Origin = Anchor.Centre; - Size = new Vector2(42); - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - cursorScale = config.GetBindable(OsuConfig.CursorSize); - - Children = new Drawable[] - { - cursorContainer = new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Scale = new Vector2((float)cursorScale), - Masking = true, - BorderThickness = Size.X / 6, - BorderColour = Color4.White, - EdgeEffect = new EdgeEffect { - Type = EdgeEffectType.Shadow, - Colour = Color4.Pink.Opacity(0.5f), - Radius = 5, - }, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, - new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = Size.X / 3, - BorderColour = Color4.White.Opacity(0.5f), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, - }, - }, - new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.1f), - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - }, - }, - } - }, - }; - cursorScale.ValueChanged += scaleChanged; - } - - private void scaleChanged(object sender, EventArgs e) - { - cursorContainer.Scale = new Vector2((float)cursorScale); - } - } - } -} diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs index 46addf5ac2..e9d84b28a3 100644 --- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs @@ -1,20 +1,21 @@ // 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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.Cursor { public class OsuTooltipContainer : TooltipContainer { - protected override Tooltip CreateTooltip() => new OsuTooltip(); + protected override ITooltip CreateTooltip() => new OsuTooltip(); public OsuTooltipContainer(CursorContainer cursor) : base(cursor) { @@ -24,6 +25,7 @@ namespace osu.Game.Graphics.Cursor { private readonly Box background; private readonly OsuSpriteText text; + private bool instantMovement = true; public override string TooltipText { @@ -32,10 +34,10 @@ namespace osu.Game.Graphics.Cursor if (value == text.Text) return; text.Text = value; - if (Alpha > 0) + if (IsPresent) { AutoSizeDuration = 250; - background.FlashColour(OsuColour.Gray(0.4f), 1000, EasingTypes.OutQuint); + background.FlashColour(OsuColour.Gray(0.4f), 1000, Easing.OutQuint); } else AutoSizeDuration = 0; @@ -46,11 +48,11 @@ namespace osu.Game.Graphics.Cursor public OsuTooltip() { - AutoSizeEasing = EasingTypes.OutQuint; + AutoSizeEasing = Easing.OutQuint; CornerRadius = 5; Masking = true; - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Colour = Color4.Black.Opacity(40), @@ -80,13 +82,23 @@ namespace osu.Game.Graphics.Cursor protected override void PopIn() { - FadeIn(500, EasingTypes.OutQuint); + instantMovement |= !IsPresent; + this.FadeIn(500, Easing.OutQuint); } - protected override void PopOut() + protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint); + + public override void Move(Vector2 pos) { - using (BeginDelayedSequence(150)) - FadeOut(500, EasingTypes.OutQuint); + if (instantMovement) + { + Position = pos; + instantMovement = false; + } + else + { + this.MoveTo(pos, 200, Easing.OutQuint); + } } } } diff --git a/osu.Game/Graphics/IHasAccentColour.cs b/osu.Game/Graphics/IHasAccentColour.cs index e4647f22fd..0c6bf98db4 100644 --- a/osu.Game/Graphics/IHasAccentColour.cs +++ b/osu.Game/Graphics/IHasAccentColour.cs @@ -3,7 +3,7 @@ using OpenTK.Graphics; using osu.Framework.Graphics; -using osu.Game.Graphics.Transforms; +using osu.Framework.Graphics.Transforms; namespace osu.Game.Graphics { @@ -20,15 +20,19 @@ namespace osu.Game.Graphics public static class AccentedColourExtensions { /// - /// Tweens the accent colour of a drawable to another colour. + /// Smoothly adjusts over time. /// - /// The drawable to apply the accent colour to. - /// The new accent colour. - /// The tween duration. - /// The tween easing. - public static void FadeAccent(this IHasAccentColour accentedDrawable, Color4 newColour, double duration = 0, EasingTypes easing = EasingTypes.None) - { - accentedDrawable.TransformTo(() => accentedDrawable.AccentColour, newColour, duration, easing, new TransformAccent()); - } + /// A to which further transforms can be added. + public static TransformSequence FadeAccent(this T accentedDrawable, Color4 newColour, double duration = 0, Easing easing = Easing.None) + where T : IHasAccentColour + => accentedDrawable.TransformTo(nameof(accentedDrawable.AccentColour), newColour, duration, easing); + + /// + /// Smoothly adjusts over time. + /// + /// A to which further transforms can be added. + public static TransformSequence FadeAccent(this TransformSequence t, Color4 newColour, double duration = 0, Easing easing = Easing.None) + where T : Drawable, IHasAccentColour + => t.Append(o => o.FadeAccent(newColour, duration, easing)); } } diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index 3d83668d07..70017d3c6e 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -13,6 +13,9 @@ namespace osu.Game.Graphics public static Color4 FromHex(string hex) { + if (hex[0] == '#') + hex = hex.Substring(1); + switch (hex.Length) { default: @@ -87,5 +90,7 @@ namespace osu.Game.Graphics public readonly Color4 RedDarker = FromHex(@"870000"); public readonly Color4 ChatBlue = FromHex(@"17292e"); + + public readonly Color4 ContextMenuGray = FromHex(@"223034"); } } diff --git a/osu.Game/Graphics/Processing/RatioAdjust.cs b/osu.Game/Graphics/Processing/RatioAdjust.cs index dd039d5144..640814d8e1 100644 --- a/osu.Game/Graphics/Processing/RatioAdjust.cs +++ b/osu.Game/Graphics/Processing/RatioAdjust.cs @@ -12,7 +12,6 @@ namespace osu.Game.Graphics.Processing { public RatioAdjust() { - AlwaysReceiveInput = true; RelativeSizeAxes = Axes.Both; } diff --git a/osu.Game/Graphics/TextAwesome.cs b/osu.Game/Graphics/SpriteIcon.cs similarity index 86% rename from osu.Game/Graphics/TextAwesome.cs rename to osu.Game/Graphics/SpriteIcon.cs index 69b0217444..d49814952c 100644 --- a/osu.Game/Graphics/TextAwesome.cs +++ b/osu.Game/Graphics/SpriteIcon.cs @@ -1,13 +1,105 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Graphics.Sprites; +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics; +using osu.Framework.IO.Stores; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Caching; namespace osu.Game.Graphics { - public class TextAwesome : OsuSpriteText + public class SpriteIcon : CompositeDrawable { - //public override FontFace FontFace => (int)Icon < 0xf000 ? FontFace.OsuFont : FontFace.FontAwesome; + private readonly Sprite spriteShadow; + private readonly Sprite spriteMain; + + private Cached layout = new Cached(); + private readonly Container shadowVisibility; + + public SpriteIcon() + { + InternalChildren = new Drawable[] + { + shadowVisibility = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Child = spriteShadow = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Y = 2, + Colour = new Color4(0f, 0f, 0f, 0.2f), + }, + Alpha = 0, + }, + spriteMain = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit + }, + }; + } + + private FontStore store; + + [BackgroundDependencyLoader] + private void load(FontStore store) + { + this.store = store; + updateTexture(); + } + + private void updateTexture() + { + var texture = store?.Get(((char)icon).ToString()); + + spriteMain.Texture = texture; + spriteShadow.Texture = texture; + + if (Size == Vector2.Zero) + Size = new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0); + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & Invalidation.Colour) > 0 && Shadow) + layout.Invalidate(); + return base.Invalidate(invalidation, source, shallPropagate); + } + + protected override void Update() + { + if (!layout.IsValid) + { + //adjust shadow alpha based on highest component intensity to avoid muddy display of darker text. + //squared result for quadratic fall-off seems to give the best result. + var avgColour = (Color4)DrawInfo.Colour.AverageColour; + + spriteShadow.Alpha = (float)Math.Pow(Math.Max(Math.Max(avgColour.R, avgColour.G), avgColour.B), 2); + + layout.Validate(); + } + } + + public bool Shadow + { + get { return spriteShadow.IsPresent; } + set + { + shadowVisibility.Alpha = value ? 1 : 0; + } + } private FontAwesome icon; @@ -23,7 +115,8 @@ namespace osu.Game.Graphics if (icon == value) return; icon = value; - Text = ((char)icon).ToString(); + if (IsLoaded) + updateTexture(); } } } diff --git a/osu.Game/Graphics/Transforms/TransformAccent.cs b/osu.Game/Graphics/Transforms/TransformAccent.cs deleted file mode 100644 index 406d1ea9eb..0000000000 --- a/osu.Game/Graphics/Transforms/TransformAccent.cs +++ /dev/null @@ -1,37 +0,0 @@ -// 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.Transforms; -using osu.Framework.MathUtils; - -namespace osu.Game.Graphics.Transforms -{ - public class TransformAccent : Transform - { - /// - /// Current value of the transformed colour in linear colour space. - /// - public override Color4 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 accented = d as IHasAccentColour; - if (accented != null) - accented.AccentColour = CurrentValue; - } - } -} diff --git a/osu.Game/Graphics/UserInterface/BackButton.cs b/osu.Game/Graphics/UserInterface/BackButton.cs index 2b4b9cdb04..6a3757ec0e 100644 --- a/osu.Game/Graphics/UserInterface/BackButton.cs +++ b/osu.Game/Graphics/UserInterface/BackButton.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Graphics; namespace osu.Game.Graphics.UserInterface @@ -18,9 +17,8 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuColour colours) + private void load(OsuColour colours) { - ActivationSound = audio.Sample.Get(@"Menu/menuback"); BackgroundColour = colours.Pink; HoverColour = colours.PinkDark; } diff --git a/osu.Game/Graphics/UserInterface/Bar.cs b/osu.Game/Graphics/UserInterface/Bar.cs index 76b75f1084..20df553142 100644 --- a/osu.Game/Graphics/UserInterface/Bar.cs +++ b/osu.Game/Graphics/UserInterface/Bar.cs @@ -5,7 +5,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using System; namespace osu.Game.Graphics.UserInterface @@ -17,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface private const int resize_duration = 250; - private const EasingTypes easing = EasingTypes.InOutCubic; + private const Easing easing = Easing.InOutCubic; private float length; /// @@ -81,7 +81,7 @@ namespace osu.Game.Graphics.UserInterface background = new Box { RelativeSizeAxes = Axes.Both, - Colour = new Color4(0,0,0,0) + Colour = new Color4(0, 0, 0, 0) }, bar = new Box { diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index d0965a1861..1a980e6bd5 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -29,7 +29,7 @@ namespace osu.Game.Graphics.UserInterface base.Direction = (direction & BarDirection.Horizontal) > 0 ? FillDirection.Vertical : FillDirection.Horizontal; foreach (var bar in Children) { - bar.Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, 1.0f / Children.Count()) : new Vector2(1.0f / Children.Count(), 1); + bar.Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, 1.0f / Children.Count) : new Vector2(1.0f / Children.Count, 1); bar.Direction = direction; } } @@ -44,21 +44,33 @@ namespace osu.Game.Graphics.UserInterface { List bars = Children.ToList(); foreach (var bar in value.Select((length, index) => new { Value = length, Bar = bars.Count > index ? bars[index] : null })) + { + float length = MaxValue ?? value.Max(); + if (length != 0) + length = bar.Value / length; + + float size = value.Count(); + if (size != 0) + size = 1.0f / size; + if (bar.Bar != null) { - bar.Bar.Length = bar.Value / (MaxValue ?? value.Max()); - bar.Bar.Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, 1.0f / value.Count()) : new Vector2(1.0f / value.Count(), 1); + bar.Bar.Length = length; + bar.Bar.Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, size) : new Vector2(size, 1); } else + { Add(new Bar { RelativeSizeAxes = Axes.Both, - Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, 1.0f / value.Count()) : new Vector2(1.0f / value.Count(), 1), - Length = bar.Value / (MaxValue ?? value.Max()), + Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, size) : new Vector2(size, 1), + Length = length, Direction = Direction, }); + } + } //I'm using ToList() here because Where() returns an Enumerable which can change it's elements afterwards - Remove(Children.Where((bar, index) => index >= value.Count()).ToList()); + RemoveRange(Children.Where((bar, index) => index >= value.Count()).ToList()); } } } diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index 8d113f4918..b3e53280fb 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -28,17 +28,17 @@ namespace osu.Game.Graphics.UserInterface var tabIndex = TabContainer.IndexOf(TabMap[tab]); t.State = tIndex < tabIndex ? Visibility.Hidden : Visibility.Visible; - t.Chevron.FadeTo(tIndex <= tabIndex ? 0f : 1f, 500, EasingTypes.OutQuint); + t.Chevron.FadeTo(tIndex <= tabIndex ? 0f : 1f, 500, Easing.OutQuint); } }; } private class BreadcrumbTabItem : OsuTabItem, IStateful { - public readonly TextAwesome Chevron; + public readonly SpriteIcon Chevron; //don't allow clicking between transitions and don't make the chevron clickable - protected override bool InternalContains(Vector2 screenSpacePos) => Alpha == 1f && Text.Contains(screenSpacePos); + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Alpha == 1f && Text.ReceiveMouseInputAt(screenSpacePos); public override bool HandleInput => State == Visibility.Visible; private Visibility state; @@ -54,13 +54,13 @@ namespace osu.Game.Graphics.UserInterface if (State == Visibility.Visible) { - FadeIn(transition_duration, EasingTypes.OutQuint); - ScaleTo(new Vector2(1f), transition_duration, EasingTypes.OutQuint); + this.FadeIn(transition_duration, Easing.OutQuint); + this.ScaleTo(new Vector2(1f), transition_duration, Easing.OutQuint); } else { - FadeOut(transition_duration, EasingTypes.OutQuint); - ScaleTo(new Vector2(0.8f, 1f), transition_duration, EasingTypes.OutQuint); + this.FadeOut(transition_duration, Easing.OutQuint); + this.ScaleTo(new Vector2(0.8f, 1f), transition_duration, Easing.OutQuint); } } } @@ -69,11 +69,11 @@ namespace osu.Game.Graphics.UserInterface { Text.TextSize = 16; Padding = new MarginPadding { Right = padding + 8 }; //padding + chevron width - Add(Chevron = new TextAwesome + Add(Chevron = new SpriteIcon { Anchor = Anchor.CentreRight, Origin = Anchor.CentreLeft, - TextSize = 12, + Size = new Vector2(12), Icon = FontAwesome.fa_chevron_right, Margin = new MarginPadding { Left = padding }, Alpha = 0f, diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index c935d58a4b..1cbf77d8f2 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -5,16 +5,17 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Containers; -using osu.Framework.Audio.Sample; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Framework.Extensions.Color4Extensions; +using osu.Game.Graphics.Containers; namespace osu.Game.Graphics.UserInterface { - public class DialogButton : ClickableContainer + public class DialogButton : OsuClickableContainer { private const float hover_width = 0.9f; private const float hover_duration = 500; @@ -78,8 +79,6 @@ namespace osu.Game.Graphics.UserInterface } } - public SampleChannel SampleClick, SampleHover; - private readonly Container backgroundContainer; private readonly Container colourContainer; private readonly Container glowContainer; @@ -92,33 +91,30 @@ namespace osu.Game.Graphics.UserInterface private bool didClick; // Used for making sure that the OnMouseDown animation can call instead of OnHoverLost's when clicking - protected override bool InternalContains(Vector2 screenSpacePos) => backgroundContainer.Contains(screenSpacePos); + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => backgroundContainer.ReceiveMouseInputAt(screenSpacePos); protected override bool OnClick(Framework.Input.InputState state) { didClick = true; - colourContainer.ResizeTo(new Vector2(1.5f, 1f), click_duration, EasingTypes.In); + colourContainer.ResizeTo(new Vector2(1.5f, 1f), click_duration, Easing.In); flash(); - SampleClick?.Play(); - Action?.Invoke(); - Delay(click_duration); - Schedule(delegate { + this.Delay(click_duration).Schedule(delegate { colourContainer.ResizeTo(new Vector2(0.8f, 1f)); spriteText.Spacing = Vector2.Zero; glowContainer.FadeOut(); }); - return true; + return base.OnClick(state); } protected override bool OnHover(Framework.Input.InputState state) { - spriteText.TransformSpacingTo(hoverSpacing, hover_duration, EasingTypes.OutElastic); + spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic); - colourContainer.ResizeTo(new Vector2(hover_width, 1f), hover_duration, EasingTypes.OutElastic); - glowContainer.FadeIn(glow_fade_duration, EasingTypes.Out); - SampleHover?.Play(); + colourContainer.ResizeTo(new Vector2(hover_width, 1f), hover_duration, Easing.OutElastic); + glowContainer.FadeIn(glow_fade_duration, Easing.Out); + base.OnHover(state); return true; } @@ -126,9 +122,9 @@ namespace osu.Game.Graphics.UserInterface { if (!didClick) { - colourContainer.ResizeTo(new Vector2(0.8f, 1f), hover_duration, EasingTypes.OutElastic); - spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, EasingTypes.OutElastic); - glowContainer.FadeOut(glow_fade_duration, EasingTypes.Out); + colourContainer.ResizeTo(new Vector2(0.8f, 1f), hover_duration, Easing.OutElastic); + spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic); + glowContainer.FadeOut(glow_fade_duration, Easing.Out); } didClick = false; @@ -152,9 +148,9 @@ namespace osu.Game.Graphics.UserInterface private void updateGlow() { - leftGlow.ColourInfo = ColourInfo.GradientHorizontal(new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f), ButtonColour); + leftGlow.Colour = ColourInfo.GradientHorizontal(new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f), ButtonColour); centerGlow.Colour = ButtonColour; - rightGlow.ColourInfo = ColourInfo.GradientHorizontal(ButtonColour, new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f)); + rightGlow.Colour = ColourInfo.GradientHorizontal(ButtonColour, new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f)); } public DialogButton() @@ -222,7 +218,7 @@ namespace osu.Game.Graphics.UserInterface Width = 0.8f, Masking = true, MaskingSmoothness = 2, - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Colour = Color4.Black.Opacity(0.2f), diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 42fff0f258..fe060f70f0 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -3,13 +3,14 @@ using OpenTK.Graphics; using OpenTK.Input; -using osu.Framework.Allocation; using osu.Framework.Input; using System; -using System.Linq; namespace osu.Game.Graphics.UserInterface { + /// + /// A textbox which holds focus eagerly. + /// public class FocusedTextBox : OsuTextBox { protected override Color4 BackgroundUnfocused => new Color4(10, 10, 10, 255); @@ -25,34 +26,28 @@ namespace osu.Game.Graphics.UserInterface { focus = value; if (!focus && HasFocus) - inputManager.ChangeFocus(null); + GetContainingInputManager().ChangeFocus(null); } } - private InputManager inputManager; - - [BackgroundDependencyLoader] - private void load(UserInputManager inputManager) - { - this.inputManager = inputManager; - } - protected override void OnFocus(InputState state) { base.OnFocus(state); BorderThickness = 0; } - protected override void OnFocusLost(InputState state) + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - if (state.Keyboard.Keys.Any(key => key == Key.Escape)) + if (args.Key == Key.Escape) { if (Text.Length > 0) Text = string.Empty; else Exit?.Invoke(); + return true; } - base.OnFocusLost(state); + + return base.OnKeyDown(state, args); } public override bool RequestsFocus => HoldFocus; diff --git a/osu.Game/Graphics/UserInterface/IconButton.cs b/osu.Game/Graphics/UserInterface/IconButton.cs new file mode 100644 index 0000000000..1808dc4b6c --- /dev/null +++ b/osu.Game/Graphics/UserInterface/IconButton.cs @@ -0,0 +1,117 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Graphics.UserInterface +{ + public class IconButton : OsuClickableContainer + { + private readonly SpriteIcon icon; + private readonly Box hover; + private readonly Container content; + + public FontAwesome Icon + { + get { return icon.Icon; } + set { icon.Icon = value; } + } + + private const float button_size = 30; + private Color4 flashColour; + + public Vector2 IconScale + { + get { return icon.Scale; } + set { icon.Scale = value; } + } + + public IconButton() + { + AutoSizeAxes = Axes.Both; + + Origin = Anchor.Centre; + Anchor = Anchor.Centre; + + Children = new Drawable[] + { + content = new Container + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(button_size), + + CornerRadius = 5, + Masking = true, + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.04f), + Type = EdgeEffectType.Shadow, + Radius = 5, + }, + Children = new Drawable[] + { + hover = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + icon = new SpriteIcon + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Size = new Vector2(18), + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + hover.Colour = colours.Yellow.Opacity(0.6f); + flashColour = colours.Yellow; + + Enabled.ValueChanged += enabled => this.FadeColour(enabled ? Color4.White : colours.Gray9, 200, Easing.OutQuint); + } + + protected override bool OnHover(InputState state) + { + hover.FadeIn(500, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + hover.FadeOut(500, Easing.OutQuint); + base.OnHoverLost(state); + } + + protected override bool OnClick(InputState state) + { + hover.FlashColour(flashColour, 800, Easing.OutQuint); + return base.OnClick(state); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + content.ScaleTo(0.75f, 2000, Easing.OutQuint); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + content.ScaleTo(1, 1000, Easing.OutElastic); + return base.OnMouseUp(state, args); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs new file mode 100644 index 0000000000..aa9256e576 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/LineGraph.cs @@ -0,0 +1,102 @@ +// 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 OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Lines; + +namespace osu.Game.Graphics.UserInterface +{ + public class LineGraph : Container + { + /// + /// Manually set the max value, otherwise will be used. + /// + public float? MaxValue { get; set; } + + /// + /// Manually set the min value, otherwise will be used. + /// + public float? MinValue { get; set; } + + public float ActualMaxValue { get; private set; } = float.NaN; + public float ActualMinValue { get; private set; } = float.NaN; + + private const double transform_duration = 1500; + + /// + /// Hold an empty area if values are less. + /// + public int DefaultValueCount; + + private readonly Container maskingContainer; + private readonly Path path; + + private float[] values; + + /// + /// A list of floats decides position of each line node. + /// + public IEnumerable Values + { + get { return values; } + set + { + values = value.ToArray(); + applyPath(); + maskingContainer.Width = 0; + maskingContainer.ResizeWidthTo(1, transform_duration, Easing.OutQuint); + } + } + + public LineGraph() + { + Add(maskingContainer = new Container + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Child = path = new Path { RelativeSizeAxes = Axes.Both, PathWidth = 1 } + }); + } + + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) + { + if ((invalidation & Invalidation.DrawSize) != 0) + applyPath(); + return base.Invalidate(invalidation, source, shallPropagate); + } + + private void applyPath() + { + path.ClearVertices(); + if (values == null) return; + + int count = Math.Max(values.Length, DefaultValueCount); + + float max = values.Max(), min = values.Min(); + if (MaxValue > max) max = MaxValue.Value; + if (MinValue < min) min = MinValue.Value; + + ActualMaxValue = max; + ActualMinValue = min; + + for (int i = 0; i < values.Length; i++) + { + float x = (i + count - values.Length) / (float)(count - 1) * DrawWidth - 1; + float y = GetYPosition(values[i]) * DrawHeight - 1; + // the -1 is for inner offset in path (actually -PathWidth) + path.AddVertex(new Vector2(x, y)); + } + } + + protected float GetYPosition(float value) + { + if (ActualMaxValue == ActualMinValue) return 0; + return (ActualMaxValue - value) / (ActualMaxValue - ActualMinValue); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/LoadingAnimation.cs b/osu.Game/Graphics/UserInterface/LoadingAnimation.cs index 61ec859b44..eb8ff5be86 100644 --- a/osu.Game/Graphics/UserInterface/LoadingAnimation.cs +++ b/osu.Game/Graphics/UserInterface/LoadingAnimation.cs @@ -1,15 +1,46 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using OpenTK; namespace osu.Game.Graphics.UserInterface { - public class LoadingAnimation : SpriteText + public class LoadingAnimation : VisibilityContainer { + private readonly SpriteIcon spinner; + public LoadingAnimation() { - Text = "Loading"; + Size = new Vector2(20); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Children = new Drawable[] + { + spinner = new SpriteIcon + { + Size = new Vector2(20), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.fa_spinner + } + }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + spinner.Spin(2000, RotationDirection.Clockwise); + } + + private const float transition_duration = 500; + + protected override void PopIn() => this.FadeIn(transition_duration * 5, Easing.OutQuint); + + protected override void PopOut() => this.FadeOut(transition_duration, Easing.OutQuint); } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Menu/MenuVisualisation.cs b/osu.Game/Graphics/UserInterface/MenuItemType.cs similarity index 54% rename from osu.Game/Screens/Menu/MenuVisualisation.cs rename to osu.Game/Graphics/UserInterface/MenuItemType.cs index 85c65b460d..bd89dbfced 100644 --- a/osu.Game/Screens/Menu/MenuVisualisation.cs +++ b/osu.Game/Graphics/UserInterface/MenuItemType.cs @@ -1,11 +1,12 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics; - -namespace osu.Game.Screens.Menu +namespace osu.Game.Graphics.UserInterface { - internal class MenuVisualisation : Drawable + public enum MenuItemType { + Standard, + Highlighted, + Destructive, } -} +} \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs index 82ede8f079..f7df6cd4f4 100644 --- a/osu.Game/Graphics/UserInterface/Nub.cs +++ b/osu.Game/Graphics/UserInterface/Nub.cs @@ -7,18 +7,17 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterface { - public class Nub : CircularContainer, IHasCurrentValue + public class Nub : CircularContainer, IHasCurrentValue, IHasAccentColour { public const float COLLAPSED_SIZE = 20; public const float EXPANDED_SIZE = 40; private const float border_width = 3; - private Color4 glowingColour, idleColour; public Nub() { @@ -44,42 +43,50 @@ namespace osu.Game.Graphics.UserInterface Current.ValueChanged += newValue => { if (newValue) - fill.FadeIn(200, EasingTypes.OutQuint); + fill.FadeIn(200, Easing.OutQuint); else - fill.FadeTo(0.01f, 200, EasingTypes.OutQuint); //todo: remove once we figure why containers aren't drawing at all times + fill.FadeTo(0.01f, 200, Easing.OutQuint); //todo: remove once we figure why containers aren't drawing at all times }; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - Colour = idleColour = colours.Pink; - glowingColour = colours.PinkLighter; + AccentColour = colours.Pink; + GlowingAccentColour = colours.PinkLighter; + GlowColour = colours.PinkDarker; - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { - Colour = colours.PinkDarker, + Colour = GlowColour, Type = EdgeEffectType.Glow, Radius = 10, Roundness = 8, }; + } + protected override void LoadComplete() + { FadeEdgeEffectTo(0); } + private bool glowing; public bool Glowing { + get { return glowing; } set { + glowing = value; + if (value) { - FadeColour(glowingColour, 500, EasingTypes.OutQuint); - FadeEdgeEffectTo(1, 500, EasingTypes.OutQuint); + this.FadeColour(GlowingAccentColour, 500, Easing.OutQuint); + FadeEdgeEffectTo(1, 500, Easing.OutQuint); } else { FadeEdgeEffectTo(0, 500); - FadeColour(idleColour, 500); + this.FadeColour(AccentColour, 500); } } } @@ -88,10 +95,48 @@ namespace osu.Game.Graphics.UserInterface { set { - ResizeTo(new Vector2(value ? EXPANDED_SIZE : COLLAPSED_SIZE, 12), 500, EasingTypes.OutQuint); + this.ResizeTo(new Vector2(value ? EXPANDED_SIZE : COLLAPSED_SIZE, 12), 500, Easing.OutQuint); } } public Bindable Current { get; } = new Bindable(); + + private Color4 accentColour; + public Color4 AccentColour + { + get { return accentColour; } + set + { + accentColour = value; + if (!Glowing) + Colour = value; + } + } + + private Color4 glowingAccentColour; + public Color4 GlowingAccentColour + { + get { return glowingAccentColour; } + set + { + glowingAccentColour = value; + if (Glowing) + Colour = value; + } + } + + private Color4 glowColour; + public Color4 GlowColour + { + get { return glowColour; } + set + { + glowColour = value; + + var effect = EdgeEffect; + effect.Colour = value; + EdgeEffect = effect; + } + } } } diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index bee12133ff..5650fc6f48 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -3,8 +3,12 @@ using OpenTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; @@ -13,10 +17,13 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface { - public class OsuButton : Button + public class OsuButton : Button, IFilterable { private Box hover; + private SampleChannel sampleClick; + private SampleChannel sampleHover; + public OsuButton() { Height = 40; @@ -33,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface public override bool HandleInput => Action != null; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { if (Action == null) Colour = OsuColour.Gray(0.5f); @@ -43,7 +50,7 @@ namespace osu.Game.Graphics.UserInterface Content.Masking = true; Content.CornerRadius = 5; - Add(new Drawable[] + AddRange(new Drawable[] { new Triangles { @@ -59,10 +66,28 @@ namespace osu.Game.Graphics.UserInterface Alpha = 0, }, }); + + sampleClick = audio.Sample.Get(@"UI/generic-click"); + sampleHover = audio.Sample.Get(@"UI/generic-hover"); + + Enabled.ValueChanged += enabled_ValueChanged; + Enabled.TriggerChange(); + } + + private void enabled_ValueChanged(bool enabled) + { + this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint); + } + + protected override bool OnClick(InputState state) + { + sampleClick?.Play(); + return base.OnClick(state); } protected override bool OnHover(InputState state) { + sampleHover?.Play(); hover.FadeIn(200); return base.OnHover(state); } @@ -75,14 +100,24 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - Content.ScaleTo(0.9f, 4000, EasingTypes.OutQuint); + Content.ScaleTo(0.9f, 4000, Easing.OutQuint); return base.OnMouseDown(state, args); } protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) { - Content.ScaleTo(1, 1000, EasingTypes.OutElastic); + Content.ScaleTo(1, 1000, Easing.OutElastic); return base.OnMouseUp(state, args); } + + public string[] FilterTerms => new[] { Text }; + + public bool MatchingFilter + { + set + { + this.FadeTo(value ? 1 : 0); + } + } } } \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 85231ffab9..68ff99e593 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -51,7 +51,8 @@ namespace osu.Game.Graphics.UserInterface } } - private readonly Nub nub; + protected readonly Nub Nub; + private readonly SpriteText labelSpriteText; private SampleChannel sampleChecked; private SampleChannel sampleUnchecked; @@ -64,7 +65,7 @@ namespace osu.Game.Graphics.UserInterface Children = new Drawable[] { labelSpriteText = new OsuSpriteText(), - nub = new Nub + Nub = new Nub { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -72,7 +73,7 @@ namespace osu.Game.Graphics.UserInterface } }; - nub.Current.BindTo(Current); + Nub.Current.BindTo(Current); Current.ValueChanged += newValue => { @@ -90,23 +91,23 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(InputState state) { - nub.Glowing = true; - nub.Expanded = true; + Nub.Glowing = true; + Nub.Expanded = true; return base.OnHover(state); } protected override void OnHoverLost(InputState state) { - nub.Glowing = false; - nub.Expanded = false; + Nub.Glowing = false; + Nub.Expanded = false; base.OnHoverLost(state); } [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleChecked = audio.Sample.Get(@"Checkbox/check-on"); - sampleUnchecked = audio.Sample.Get(@"Checkbox/check-off"); + sampleChecked = audio.Sample.Get(@"UI/check-on"); + sampleUnchecked = audio.Sample.Get(@"UI/check-off"); } } } diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs new file mode 100644 index 0000000000..d4882cce1e --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -0,0 +1,52 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; + +namespace osu.Game.Graphics.UserInterface +{ + public class OsuContextMenu : ContextMenu + where TItem : ContextMenuItem + { + protected override Menu CreateMenu() => new CustomMenu(); + + public class CustomMenu : Menu + { + private const int fade_duration = 250; + + public CustomMenu() + { + CornerRadius = 5; + ItemsContainer.Padding = new MarginPadding { Vertical = OsuContextMenuItem.MARGIN_VERTICAL }; + Masking = true; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.1f), + Radius = 4, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Background.Colour = colours.ContextMenuGray; + } + + protected override void AnimateOpen() => this.FadeIn(fade_duration, Easing.OutQuint); + protected override void AnimateClose() => this.FadeOut(fade_duration, Easing.OutQuint); + + protected override void UpdateContentHeight() + { + var actualHeight = (RelativeSizeAxes & Axes.Y) > 0 ? 1 : ContentHeight; + this.ResizeTo(new Vector2(1, State == MenuState.Opened ? actualHeight : 0), 300, Easing.OutQuint); + } + } + } +} \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenuItem.cs b/osu.Game/Graphics/UserInterface/OsuContextMenuItem.cs new file mode 100644 index 0000000000..196e905bdc --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuContextMenuItem.cs @@ -0,0 +1,114 @@ +// 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.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics.UserInterface +{ + public class OsuContextMenuItem : ContextMenuItem + { + private const int transition_length = 80; + private const int margin_horizontal = 17; + public const int MARGIN_VERTICAL = 4; + private const int text_size = 17; + + private OsuSpriteText text; + private OsuSpriteText textBold; + + private SampleChannel sampleClick; + private SampleChannel sampleHover; + + private readonly MenuItemType type; + + protected override Container CreateTextContainer(string title) => new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Children = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TextSize = text_size, + Text = title, + Margin = new MarginPadding { Horizontal = margin_horizontal, Vertical = MARGIN_VERTICAL }, + }, + textBold = new OsuSpriteText + { + AlwaysPresent = true, + Alpha = 0, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TextSize = text_size, + Text = title, + Font = @"Exo2.0-Bold", + Margin = new MarginPadding { Horizontal = margin_horizontal, Vertical = MARGIN_VERTICAL }, + } + } + }; + + public OsuContextMenuItem(string title, MenuItemType type = MenuItemType.Standard) : base(title) + { + this.type = type; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleHover = audio.Sample.Get(@"UI/generic-hover"); + sampleClick = audio.Sample.Get(@"UI/generic-click"); + + BackgroundColour = Color4.Transparent; + BackgroundColourHover = OsuColour.FromHex(@"172023"); + + updateTextColour(); + } + + private void updateTextColour() + { + switch (type) + { + case MenuItemType.Standard: + textBold.Colour = text.Colour = Color4.White; + break; + case MenuItemType.Destructive: + textBold.Colour = text.Colour = Color4.Red; + break; + case MenuItemType.Highlighted: + textBold.Colour = text.Colour = OsuColour.FromHex(@"ffcc22"); + break; + } + } + + protected override bool OnHover(InputState state) + { + sampleHover.Play(); + textBold.FadeIn(transition_length, Easing.OutQuint); + text.FadeOut(transition_length, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + textBold.FadeOut(transition_length, Easing.OutQuint); + text.FadeIn(transition_length, Easing.OutQuint); + base.OnHoverLost(state); + } + + protected override bool OnClick(InputState state) + { + sampleClick.Play(); + return base.OnClick(state); + } + } +} \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 6dadd63ac4..f5a4219707 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.Sprites; +using OpenTK; namespace osu.Game.Graphics.UserInterface { @@ -60,14 +61,13 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Y, Children = new Drawable[] { - Chevron = new TextAwesome + Chevron = new SpriteIcon { AlwaysPresent = true, Icon = FontAwesome.fa_chevron_right, - UseFullGlyphHeight = false, Colour = Color4.Black, Alpha = 0.5f, - TextSize = 8, + Size = new Vector2(8), Margin = new MarginPadding { Left = 3, Right = 3 }, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, @@ -84,7 +84,7 @@ namespace osu.Game.Graphics.UserInterface private Color4? accentColour; - protected readonly TextAwesome Chevron; + protected readonly SpriteIcon Chevron; protected readonly OsuSpriteText Label; protected override void FormatForeground(bool hover = false) @@ -123,7 +123,7 @@ namespace osu.Game.Graphics.UserInterface set { Text.Text = value; } } - protected readonly TextAwesome Icon; + protected readonly SpriteIcon Icon; private Color4? accentColour; public virtual Color4 AccentColour @@ -152,13 +152,13 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - Icon = new TextAwesome + Icon = new SpriteIcon { Icon = FontAwesome.fa_chevron_down, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding { Right = 4 }, - TextSize = 20 + Size = new Vector2(20), } }; } diff --git a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs index 5de6507bb3..bf835f0165 100644 --- a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs @@ -16,7 +16,7 @@ namespace osu.Game.Graphics.UserInterface throw new InvalidOperationException("OsuEnumDropdown only supports enums as the generic type argument"); List> items = new List>(); - foreach(var val in (T[])Enum.GetValues(typeof(T))) + foreach (var val in (T[])Enum.GetValues(typeof(T))) { var field = typeof(T).GetField(Enum.GetName(typeof(T), val)); items.Add( diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 84b88da96e..e597bc44b5 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -19,14 +19,14 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding(5); } - protected override void AnimateOpen() => FadeIn(300, EasingTypes.OutQuint); + protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint); - protected override void AnimateClose() => FadeOut(300, EasingTypes.OutQuint); + protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint); protected override void UpdateContentHeight() { var actualHeight = (RelativeSizeAxes & Axes.Y) > 0 ? 1 : ContentHeight; - ResizeTo(new Vector2(1, State == MenuState.Opened ? actualHeight : 0), 300, EasingTypes.OutQuint); + this.ResizeTo(new Vector2(1, State == MenuState.Opened ? actualHeight : 0), 300, Easing.OutQuint); } } } diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index 8ad3febf19..70879006e9 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -7,8 +7,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Sprites; using System; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Graphics.UserInterface { @@ -58,8 +58,8 @@ namespace osu.Game.Graphics.UserInterface protected override void LoadComplete() { base.LoadComplete(); - circle.FadeIn(500, EasingTypes.OutQuint); - circle.ResizeTo(new Vector2(0.8f), 500, EasingTypes.OutQuint); + circle.FadeIn(500, Easing.OutQuint); + circle.ResizeTo(new Vector2(0.8f), 500, Easing.OutQuint); } } diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index c8be085b51..3dd3596c30 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -3,26 +3,27 @@ using System; using OpenTK; +using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Graphics.UserInterface { - public class OsuSliderBar : SliderBar, IHasTooltip + public class OsuSliderBar : SliderBar, IHasTooltip, IHasAccentColour where T : struct, IEquatable { private SampleChannel sample; private double lastSampleTime; private T lastSampleValue; - private readonly Nub nub; + protected readonly Nub Nub; private readonly Box leftBox; private readonly Box rightBox; @@ -46,6 +47,18 @@ namespace osu.Game.Graphics.UserInterface } } + private Color4 accentColour; + public Color4 AccentColour + { + get { return accentColour; } + set + { + accentColour = value; + leftBox.Colour = value; + rightBox.Colour = value; + } + } + public OsuSliderBar() { Height = 12; @@ -71,7 +84,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.CentreRight, Alpha = 0.5f, }, - nub = new Nub + Nub = new Nub { Origin = Anchor.TopCentre, Expanded = true, @@ -87,20 +100,19 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio, OsuColour colours) { - sample = audio.Sample.Get(@"Sliderbar/sliderbar"); - leftBox.Colour = colours.Pink; - rightBox.Colour = colours.Pink; + sample = audio.Sample.Get(@"UI/sliderbar-notch"); + AccentColour = colours.Pink; } protected override bool OnHover(InputState state) { - nub.Glowing = true; + Nub.Glowing = true; return base.OnHover(state); } protected override void OnHoverLost(InputState state) { - nub.Glowing = false; + Nub.Glowing = false; base.OnHoverLost(state); } @@ -133,13 +145,13 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - nub.Current.Value = true; + Nub.Current.Value = true; return base.OnMouseDown(state, args); } protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) { - nub.Current.Value = false; + Nub.Current.Value = false; return base.OnMouseUp(state, args); } @@ -147,14 +159,14 @@ namespace osu.Game.Graphics.UserInterface { base.UpdateAfterChildren(); leftBox.Scale = new Vector2(MathHelper.Clamp( - nub.DrawPosition.X - nub.DrawWidth / 2, 0, DrawWidth), 1); + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1); rightBox.Scale = new Vector2(MathHelper.Clamp( - DrawWidth - nub.DrawPosition.X - nub.DrawWidth / 2, 0, DrawWidth), 1); + DrawWidth - Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1); } protected override void UpdateValue(float value) { - nub.MoveToX(RangePadding + UsableWidth * value, 250, EasingTypes.OutQuint); + Nub.MoveToX(RangePadding + UsableWidth * value, 250, Easing.OutQuint); } } } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 4bbae4efd1..5ad412965c 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; @@ -22,9 +23,7 @@ namespace osu.Game.Graphics.UserInterface protected override TabItem CreateTabItem(T value) => new OsuTabItem(value); - protected override bool InternalContains(Vector2 screenSpacePos) => base.InternalContains(screenSpacePos) || Dropdown.Contains(screenSpacePos); - - private bool isEnumType => typeof(T).IsEnum; + private static bool isEnumType => typeof(T).IsEnum; public OsuTabControl() { @@ -74,33 +73,18 @@ namespace osu.Game.Graphics.UserInterface } } - public override bool Active - { - get { return base.Active; } - set - { - if (Active == value) return; - - if (value) - fadeActive(); - else - fadeInactive(); - base.Active = value; - } - } - private const float transition_length = 500; private void fadeActive() { - box.FadeIn(transition_length, EasingTypes.OutQuint); - Text.FadeColour(Color4.White, transition_length, EasingTypes.OutQuint); + box.FadeIn(transition_length, Easing.OutQuint); + Text.FadeColour(Color4.White, transition_length, Easing.OutQuint); } private void fadeInactive() { - box.FadeOut(transition_length, EasingTypes.OutQuint); - Text.FadeColour(AccentColour, transition_length, EasingTypes.OutQuint); + box.FadeOut(transition_length, Easing.OutQuint); + Text.FadeColour(AccentColour, transition_length, Easing.OutQuint); } protected override bool OnHover(InputState state) @@ -150,6 +134,10 @@ namespace osu.Game.Graphics.UserInterface } }; } + + protected override void OnActivated() => fadeActive(); + + protected override void OnDeactivated() => fadeInactive(); } private class OsuTabDropdown : OsuDropdown @@ -221,10 +209,10 @@ namespace osu.Game.Graphics.UserInterface Foreground.Children = new Drawable[] { - new TextAwesome + new SpriteIcon { Icon = FontAwesome.fa_ellipsis_h, - TextSize = 14, + Size = new Vector2(14), Origin = Anchor.Centre, Anchor = Anchor.Centre, } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index 6fc3875f61..a0d48e5ebe 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -6,10 +6,11 @@ using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; -using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface { @@ -20,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface { private readonly Box box; private readonly SpriteText text; - private readonly TextAwesome icon; + private readonly SpriteIcon icon; private Color4? accentColour; public Color4 AccentColour @@ -48,14 +49,14 @@ namespace osu.Game.Graphics.UserInterface private void fadeIn() { - box.FadeIn(transition_length, EasingTypes.OutQuint); - text.FadeColour(Color4.White, transition_length, EasingTypes.OutQuint); + box.FadeIn(transition_length, Easing.OutQuint); + text.FadeColour(Color4.White, transition_length, Easing.OutQuint); } private void fadeOut() { - box.FadeOut(transition_length, EasingTypes.OutQuint); - text.FadeColour(AccentColour, transition_length, EasingTypes.OutQuint); + box.FadeOut(transition_length, Easing.OutQuint); + text.FadeColour(AccentColour, transition_length, Easing.OutQuint); } protected override bool OnHover(InputState state) @@ -98,9 +99,9 @@ namespace osu.Game.Graphics.UserInterface TextSize = 14, Font = @"Exo2.0-Bold", }, - icon = new TextAwesome + icon = new SpriteIcon { - TextSize = 14, + Size = new Vector2(14), Icon = FontAwesome.fa_circle_o, Shadow = true, }, diff --git a/osu.Game/Overlays/Direct/SortTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs similarity index 65% rename from osu.Game/Overlays/Direct/SortTabControl.cs rename to osu.Game/Graphics/UserInterface/PageTabControl.cs index 4d4e02d875..6b97e54ecd 100644 --- a/osu.Game/Overlays/Direct/SortTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -7,58 +7,44 @@ using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; -using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -namespace osu.Game.Overlays.Direct +namespace osu.Game.Graphics.UserInterface { - public class SortTabControl : OsuTabControl + public class PageTabControl : OsuTabControl { - protected override TabItem CreateTabItem(SortCriteria value) => new SortTabItem(value); + protected override TabItem CreateTabItem(T value) => new PageTabItem(value); - public SortTabControl() + public PageTabControl() { Height = 30; } - private class SortTabItem : TabItem + public class PageTabItem : TabItem { private const float transition_duration = 100; private readonly Box box; - public override bool Active - { - get { return base.Active; } - set - { - if (Active == value) return; + protected readonly SpriteText Text; - if (value) - slideActive(); - else - slideInactive(); - base.Active = value; - } - } - - public SortTabItem(SortCriteria value) : base(value) + public PageTabItem(T value) : base(value) { AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; Children = new Drawable[] { - new OsuSpriteText + Text = new OsuSpriteText { Margin = new MarginPadding { Top = 8, Bottom = 8 }, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - Text = (value as Enum).GetDescription() ?? value.ToString(), + Text = (value as Enum)?.GetDescription() ?? value.ToString(), TextSize = 14, Font = @"Exo2.0-Bold", }, @@ -102,16 +88,10 @@ namespace osu.Game.Overlays.Direct { box.ScaleTo(new Vector2(1f, 0f), transition_duration); } + + protected override void OnActivated() => slideActive(); + + protected override void OnDeactivated() => slideInactive(); } } - - public enum SortCriteria - { - Title, - Artist, - Creator, - Difficulty, - Ranked, - Rating, - } } diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index c32b654840..0a402f9045 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs @@ -1,9 +1,6 @@ // 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.Transforms; -using osu.Framework.MathUtils; using System; namespace osu.Game.Graphics.UserInterface @@ -13,8 +10,6 @@ namespace osu.Game.Graphics.UserInterface /// public class PercentageCounter : RollingCounter { - protected override Type TransformType => typeof(TransformAccuracy); - protected override double RollingDuration => 750; private float epsilon => 1e-10f; @@ -44,26 +39,5 @@ namespace osu.Game.Graphics.UserInterface { Current.Value = Current + amount; } - - protected class TransformAccuracy : 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, (float)StartValue, (float)EndValue, StartTime, EndTime, Easing); - } - } - - public override void Apply(Drawable d) - { - base.Apply(d); - ((PercentageCounter)d).DisplayedCount = CurrentValue; - } - } } } diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index bdb2054ca4..ce8d190dc0 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -5,30 +5,21 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Transforms; using osu.Game.Graphics.Sprites; using System; using System.Collections.Generic; -using System.Diagnostics; using OpenTK.Graphics; namespace osu.Game.Graphics.UserInterface { public abstract class RollingCounter : Container, IHasAccentColour + where T : struct, IEquatable { /// /// The current value. /// public Bindable Current = new Bindable(); - /// - /// Type of the Transform to use. - /// - /// - /// Must be a subclass of Transform(T) - /// - protected virtual Type TransformType => typeof(Transform); - protected SpriteText DisplayedCountSpriteText; /// @@ -45,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface /// /// Easing for the counter rollover animation. /// - protected virtual EasingTypes RollingEasing => EasingTypes.OutQuint; + protected virtual Easing RollingEasing => Easing.OutQuint; private T displayedCount; @@ -58,7 +49,8 @@ namespace osu.Game.Graphics.UserInterface { return displayedCount; } - protected set + + set { if (EqualityComparer.Default.Equals(displayedCount, value)) return; @@ -133,7 +125,7 @@ namespace osu.Game.Graphics.UserInterface /// public virtual void StopRolling() { - Flush(false, TransformType); + FinishTransforms(false, nameof(DisplayedCount)); DisplayedCount = Current; } @@ -176,45 +168,15 @@ namespace osu.Game.Graphics.UserInterface /// implement the rollover animation). /// /// Count value before modification. - /// Expected count value after modification- - /// + /// Expected count value after modification. protected virtual void TransformCount(T currentValue, T newValue) { - Debug.Assert( - typeof(Transform).IsAssignableFrom(TransformType), - @"transformType should be a subclass of Transform." - ); - - TransformCount((Transform)Activator.CreateInstance(TransformType), currentValue, newValue); - } - - /// - /// Intended to be used by TransformCount(T currentValue, T newValue). - /// - protected void TransformCount(Transform transform, T currentValue, T newValue) - { - Type type = transform.GetType(); - - Flush(false, type); - - if (RollingDuration < 1) - { - DisplayedCount = Current; - return; - } - double rollingTotalDuration = IsRollingProportional ? GetProportionalDuration(currentValue, newValue) : RollingDuration; - transform.StartTime = TransformStartTime; - transform.EndTime = TransformStartTime + rollingTotalDuration; - transform.StartValue = currentValue; - transform.EndValue = newValue; - transform.Easing = RollingEasing; - - Transforms.Add(transform); + this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing); } } } diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 3e01b9e4f4..ee4fc912f3 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -2,18 +2,13 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Framework.Graphics.Transforms; -using osu.Framework.MathUtils; -using System; namespace osu.Game.Graphics.UserInterface { public class ScoreCounter : RollingCounter { - protected override Type TransformType => typeof(TransformScore); - protected override double RollingDuration => 1000; - protected override EasingTypes RollingEasing => EasingTypes.Out; + protected override Easing RollingEasing => Easing.Out; public bool UseCommaSeparator; @@ -55,26 +50,5 @@ namespace osu.Game.Graphics.UserInterface { Current.Value = Current + amount; } - - protected class TransformScore : 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, (float)StartValue, (float)EndValue, StartTime, EndTime, Easing); - } - } - - public override void Apply(Drawable d) - { - base.Apply(d); - ((ScoreCounter)d).DisplayedCount = CurrentValue; - } - } } } diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index 0a37024d0f..ee5d3baf66 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -3,13 +3,11 @@ using osu.Framework.Graphics; using osu.Framework.Input; +using OpenTK; using OpenTK.Input; namespace osu.Game.Graphics.UserInterface { - /// - /// A textbox which holds focus eagerly. - /// public class SearchTextBox : FocusedTextBox { protected virtual bool AllowCommit => false; @@ -17,15 +15,15 @@ namespace osu.Game.Graphics.UserInterface public SearchTextBox() { Height = 35; - Add(new Drawable[] + AddRange(new Drawable[] { - new TextAwesome + new SpriteIcon { Icon = FontAwesome.fa_search, Origin = Anchor.CentreRight, Anchor = Anchor.CentreRight, Margin = new MarginPadding { Right = 10 }, - TextSize = 20 + Size = new Vector2(20), } }); @@ -45,9 +43,16 @@ namespace osu.Game.Graphics.UserInterface case Key.Up: case Key.Down: return false; + } + } + + if (!AllowCommit) + { + switch (args.Key) + { + case Key.KeypadEnter: case Key.Enter: - if (!AllowCommit) return false; - break; + return false; } } diff --git a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs index 8537e80f63..211de72efc 100644 --- a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs +++ b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs @@ -2,9 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Transforms; -using osu.Framework.MathUtils; namespace osu.Game.Graphics.UserInterface { @@ -13,8 +10,6 @@ namespace osu.Game.Graphics.UserInterface /// public class SimpleComboCounter : RollingCounter { - protected override Type TransformType => typeof(TransformCounterCount); - protected override double RollingDuration => 750; public SimpleComboCounter() @@ -36,26 +31,5 @@ namespace osu.Game.Graphics.UserInterface { Current.Value = Current + amount; } - - private class TransformCounterCount : Transform - { - public override int CurrentValue - { - get - { - double time = Time?.Current ?? 0; - if (time < StartTime) return StartValue; - if (time >= EndTime) return EndValue; - - return (int)Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); - } - } - - public override void Apply(Drawable d) - { - base.Apply(d); - ((SimpleComboCounter)d).DisplayedCount = CurrentValue; - } - } } } \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index 295cdac81d..e581d19d54 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -24,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface private double animationDelay => 80; private double scalingDuration => 1000; - private EasingTypes scalingEasing => EasingTypes.OutElasticHalf; + private Easing scalingEasing => Easing.OutElasticHalf; private float minStarScale => 0.4f; private double fadingDuration => 100; @@ -33,25 +33,25 @@ namespace osu.Game.Graphics.UserInterface private const float star_size = 20; private const float star_spacing = 4; - private float count; + private float countStars; /// /// Amount of stars represented. /// - public float Count + public float CountStars { get { - return count; + return countStars; } set { - if (count == value) return; + if (countStars == value) return; if (IsLoaded) transformCount(value); - count = value; + countStars = value; } } @@ -94,15 +94,15 @@ namespace osu.Game.Graphics.UserInterface public void ResetCount() { - count = 0; + countStars = 0; StopAnimation(); } public void ReplayAnimation() { - var t = count; + var t = countStars; ResetCount(); - Count = t; + CountStars = t; } public void StopAnimation() @@ -111,8 +111,8 @@ namespace osu.Game.Graphics.UserInterface foreach (var star in stars.Children) { star.ClearTransforms(true); - star.FadeTo(i < count ? 1.0f : minStarAlpha); - star.Icon.ScaleTo(getStarScale(i, count)); + star.FadeTo(i < countStars ? 1.0f : minStarAlpha); + star.Icon.ScaleTo(getStarScale(i, countStars)); i++; } } @@ -122,7 +122,7 @@ namespace osu.Game.Graphics.UserInterface if (value <= i) return minStarScale; - return i + 1 <= value ? 1.0f : (float)Interpolation.ValueAt(value, minStarScale, 1.0f, i, i + 1); + return i + 1 <= value ? 1.0f : Interpolation.ValueAt(value, minStarScale, 1.0f, i, i + 1); } private void transformCount(float newValue) @@ -132,13 +132,9 @@ namespace osu.Game.Graphics.UserInterface { star.ClearTransforms(true); - var delay = (count <= newValue ? Math.Max(i - count, 0) : Math.Max(count - 1 - i, 0)) * animationDelay; - - using (BeginDelayedSequence(delay, true)) - { - star.FadeTo(i < newValue ? 1.0f : minStarAlpha, fadingDuration); - star.Icon.ScaleTo(getStarScale(i, newValue), scalingDuration, scalingEasing); - } + var delay = (countStars <= newValue ? Math.Max(i - countStars, 0) : Math.Max(countStars - 1 - i, 0)) * animationDelay; + star.Delay(delay).FadeTo(i < newValue ? 1.0f : minStarAlpha, fadingDuration); + star.Icon.Delay(delay).ScaleTo(getStarScale(i, newValue), scalingDuration, scalingEasing); i++; } @@ -146,16 +142,16 @@ namespace osu.Game.Graphics.UserInterface private class Star : Container { - public readonly TextAwesome Icon; + public readonly SpriteIcon Icon; public Star() { Size = new Vector2(star_size); Children = new[] { - Icon = new TextAwesome + Icon = new SpriteIcon { - TextSize = star_size, + Size = new Vector2(star_size), Icon = FontAwesome.fa_star, Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index ebaef661c4..f17b307826 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -14,10 +13,11 @@ using osu.Game.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; using osu.Framework.Audio.Track; using System; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Graphics.UserInterface { - public class TwoLayerButton : ClickableContainer + public class TwoLayerButton : OsuClickableContainer { private readonly BouncingIcon bouncingIcon; @@ -31,7 +31,6 @@ namespace osu.Game.Graphics.UserInterface public static readonly Vector2 SIZE_EXTENDED = new Vector2(140, 50); public static readonly Vector2 SIZE_RETRACTED = new Vector2(100, 50); - public SampleChannel ActivationSound; private readonly SpriteText text; public Color4 HoverColour; @@ -62,8 +61,12 @@ namespace osu.Game.Graphics.UserInterface X = (value & Anchor.x2) > 0 ? SIZE_RETRACTED.X * shear * 0.5f : 0; + Remove(c1); + Remove(c2); c1.Depth = (value & Anchor.x2) > 0 ? 0 : 1; c2.Depth = (value & Anchor.x2) > 0 ? 1 : 0; + Add(c1); + Add(c2); } } @@ -79,18 +82,21 @@ namespace osu.Game.Graphics.UserInterface Width = 0.4f, Children = new Drawable[] { - new Container { + new Container + { RelativeSizeAxes = Axes.Both, Shear = new Vector2(shear, 0), Masking = true, MaskingSmoothness = 2, - EdgeEffect = new EdgeEffect { + EdgeEffect = new EdgeEffectParameters + { Type = EdgeEffectType.Shadow, Colour = Color4.Black.Opacity(0.2f), Offset = new Vector2(2, 0), Radius = 2, }, - Children = new [] { + Children = new[] + { IconLayer = new Box { RelativeSizeAxes = Axes.Both, @@ -113,18 +119,21 @@ namespace osu.Game.Graphics.UserInterface Width = 0.6f, Children = new Drawable[] { - new Container { + new Container + { RelativeSizeAxes = Axes.Both, Shear = new Vector2(shear, 0), Masking = true, MaskingSmoothness = 2, - EdgeEffect = new EdgeEffect { + EdgeEffect = new EdgeEffectParameters + { Type = EdgeEffectType.Shadow, Colour = Color4.Black.Opacity(0.2f), Offset = new Vector2(2, 0), Radius = 2, }, - Children = new [] { + Children = new[] + { TextLayer = new Box { Origin = Anchor.TopLeft, @@ -160,24 +169,24 @@ namespace osu.Game.Graphics.UserInterface } } - protected override bool InternalContains(Vector2 screenSpacePos) => IconLayer.Contains(screenSpacePos) || TextLayer.Contains(screenSpacePos); + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => IconLayer.ReceiveMouseInputAt(screenSpacePos) || TextLayer.ReceiveMouseInputAt(screenSpacePos); protected override bool OnHover(InputState state) { - ResizeTo(SIZE_EXTENDED, transform_time, EasingTypes.OutElastic); - IconLayer.FadeColour(HoverColour, transform_time, EasingTypes.OutElastic); + this.ResizeTo(SIZE_EXTENDED, transform_time, Easing.OutElastic); + IconLayer.FadeColour(HoverColour, transform_time, Easing.OutElastic); - bouncingIcon.ScaleTo(1.1f, transform_time, EasingTypes.OutElastic); + bouncingIcon.ScaleTo(1.1f, transform_time, Easing.OutElastic); return true; } protected override void OnHoverLost(InputState state) { - ResizeTo(SIZE_RETRACTED, transform_time, EasingTypes.OutElastic); - IconLayer.FadeColour(TextLayer.Colour, transform_time, EasingTypes.OutElastic); + this.ResizeTo(SIZE_RETRACTED, transform_time, Easing.OutElastic); + IconLayer.FadeColour(TextLayer.Colour, transform_time, Easing.OutElastic); - bouncingIcon.ScaleTo(1, transform_time, EasingTypes.OutElastic); + bouncingIcon.ScaleTo(1, transform_time, Easing.OutElastic); } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) @@ -196,11 +205,9 @@ namespace osu.Game.Graphics.UserInterface Add(flash); flash.Alpha = 1; - flash.FadeOut(500, EasingTypes.OutQuint); + flash.FadeOut(500, Easing.OutQuint); flash.Expire(); - ActivationSound.Play(); - return base.OnClick(state); } @@ -208,7 +215,7 @@ namespace osu.Game.Graphics.UserInterface { private const double beat_in_time = 60; - private readonly TextAwesome icon; + private readonly SpriteIcon icon; public FontAwesome Icon { set { icon.Icon = value; } } @@ -219,11 +226,11 @@ namespace osu.Game.Graphics.UserInterface Children = new Drawable[] { - icon = new TextAwesome + icon = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - TextSize = 25 + Size = new Vector2(25), } }; } @@ -238,9 +245,9 @@ namespace osu.Game.Graphics.UserInterface if (beatIndex < 0) return; - icon.ScaleTo(1 - 0.1f * amplitudeAdjust, beat_in_time, EasingTypes.Out); - using (icon.BeginDelayedSequence(beat_in_time)) - icon.ScaleTo(1, beatLength * 2, EasingTypes.OutQuint); + icon.ScaleTo(1 - 0.1f * amplitudeAdjust, beat_in_time, Easing.Out) + .Then() + .ScaleTo(1, beatLength * 2, Easing.OutQuint); } } } diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs index a758d5fdef..4c108e793a 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs @@ -3,11 +3,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; using osu.Framework.Threading; using OpenTK; using osu.Framework.Audio; using osu.Framework.Allocation; +using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface.Volume { @@ -15,8 +15,6 @@ namespace osu.Game.Graphics.UserInterface.Volume { private readonly VolumeMeter volumeMeterMaster; - protected override bool HideOnEscape => false; - private void volumeChanged(double newVolume) { Show(); @@ -66,15 +64,25 @@ namespace osu.Game.Graphics.UserInterface.Volume volumeMeterMusic.Bindable.ValueChanged -= volumeChanged; } - public void Adjust(InputState state) + public bool Adjust(GlobalAction action) { - if (State == Visibility.Hidden) + switch (action) { - Show(); - return; + case GlobalAction.DecreaseVolume: + if (State == Visibility.Hidden) + Show(); + else + volumeMeterMaster.Decrease(); + return true; + case GlobalAction.IncreaseVolume: + if (State == Visibility.Hidden) + Show(); + else + volumeMeterMaster.Increase(); + return true; } - volumeMeterMaster.TriggerOnWheel(state); + return false; } [BackgroundDependencyLoader] @@ -93,21 +101,20 @@ namespace osu.Game.Graphics.UserInterface.Volume protected override void PopIn() { ClearTransforms(); - FadeIn(100); + this.FadeIn(100); schedulePopOut(); } protected override void PopOut() { - FadeOut(100); + this.FadeOut(100); } private void schedulePopOut() { popOutDelegate?.Cancel(); - Delay(1000); - popOutDelegate = Schedule(Hide); + this.Delay(1000).Schedule(Hide, out popOutDelegate); } } } \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs index c155871f33..c222fecb5d 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs @@ -3,32 +3,16 @@ using System; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using OpenTK.Input; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface.Volume { - internal class VolumeControlReceptor : Container + internal class VolumeControlReceptor : Container, IKeyBindingHandler { - public Action ActionRequested; + public Func ActionRequested; - protected override bool OnWheel(InputState state) - { - ActionRequested?.Invoke(state); - return true; - } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - switch (args.Key) - { - case Key.Up: - case Key.Down: - ActionRequested?.Invoke(state); - return true; - } - - return base.OnKeyDown(state, args); - } + public bool OnPressed(GlobalAction action) => ActionRequested?.Invoke(action) ?? false; + public bool OnReleased(GlobalAction action) => false; } } diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs index fd1d890330..81c4fa9bae 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs @@ -4,15 +4,16 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; using osu.Game.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface.Volume { - internal class VolumeMeter : Container + internal class VolumeMeter : Container, IKeyBindingHandler { private readonly Box meterFill; public BindableDouble Bindable { get; } = new BindableDouble(); @@ -76,12 +77,35 @@ namespace osu.Game.Graphics.UserInterface.Volume } } - protected override bool OnWheel(InputState state) + public void Increase() { - Volume += 0.05f * state.Mouse.WheelDelta; - return true; + Volume += 0.05f; } - private void updateFill() => meterFill.ScaleTo(new Vector2(1, (float)Volume), 300, EasingTypes.OutQuint); + public void Decrease() + { + Volume -= 0.05f; + } + + private void updateFill() => meterFill.ScaleTo(new Vector2(1, (float)Volume), 300, Easing.OutQuint); + + public bool OnPressed(GlobalAction action) + { + if (!IsHovered) return false; + + switch (action) + { + case GlobalAction.DecreaseVolume: + Decrease(); + return true; + case GlobalAction.IncreaseVolume: + Increase(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; } } \ No newline at end of file diff --git a/osu.Game/IO/FileInfo.cs b/osu.Game/IO/FileInfo.cs new file mode 100644 index 0000000000..367fd68f7b --- /dev/null +++ b/osu.Game/IO/FileInfo.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.IO; +using SQLite.Net.Attributes; + +namespace osu.Game.IO +{ + public class FileInfo + { + [PrimaryKey, AutoIncrement] + public int ID { get; set; } + + [Indexed(Unique = true)] + public string Hash { get; set; } + + public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash); + + [Indexed] + public int ReferenceCount { get; set; } + } +} diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs new file mode 100644 index 0000000000..1011fa3236 --- /dev/null +++ b/osu.Game/IO/FileStore.cs @@ -0,0 +1,160 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.IO; +using System.Linq; +using osu.Framework.Extensions; +using osu.Framework.IO.Stores; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Database; +using SQLite.Net; + +namespace osu.Game.IO +{ + /// + /// Handles the Store and retrieval of Files/FileSets to the database backing + /// + public class FileStore : DatabaseBackedStore + { + private const string prefix = "files"; + + public readonly ResourceStore Store; + + protected override int StoreVersion => 2; + + public FileStore(SQLiteConnection connection, Storage storage) : base(connection, storage) + { + Store = new NamespacedResourceStore(new StorageBackedResourceStore(storage), prefix); + } + + protected override Type[] ValidTypes => new[] { + typeof(FileInfo), + }; + + protected override void Prepare(bool reset = false) + { + if (reset) + { + // in earlier versions we stored beatmaps as solid archives, but not any more. + if (Storage.ExistsDirectory("beatmaps")) + Storage.DeleteDirectory("beatmaps"); + + if (Storage.ExistsDirectory(prefix)) + Storage.DeleteDirectory(prefix); + + Connection.DropTable(); + } + + Connection.CreateTable(); + } + + protected override void StartupTasks() + { + base.StartupTasks(); + deletePending(); + } + + /// + /// Perform migrations between two store versions. + /// + /// The current store version. This will be zero on a fresh database initialisation. + /// The target version which we are migrating to (equal to the current ). + protected override void PerformMigration(int currentVersion, int targetVersion) + { + base.PerformMigration(currentVersion, targetVersion); + + while (currentVersion++ < targetVersion) + { + switch (currentVersion) + { + case 1: + case 2: + // cannot migrate; breaking underlying changes. + Reset(); + break; + } + } + } + + public FileInfo Add(Stream data) + { + string hash = data.ComputeSHA2Hash(); + + var existing = Connection.Table().Where(f => f.Hash == hash).FirstOrDefault(); + + var info = existing ?? new FileInfo { Hash = hash }; + if (existing != null) + { + info = existing; + } + else + { + string path = Path.Combine(prefix, info.StoragePath); + + data.Seek(0, SeekOrigin.Begin); + + if (!Storage.Exists(path)) + using (var output = Storage.GetStream(path, FileAccess.Write)) + data.CopyTo(output); + + data.Seek(0, SeekOrigin.Begin); + + Connection.Insert(info); + } + + Reference(info); + return info; + } + + public void Reference(params FileInfo[] files) + { + Connection.RunInTransaction(() => + { + var incrementedFiles = files.GroupBy(f => f.ID).Select(f => + { + var accurateRefCount = Connection.Get(f.First().ID); + accurateRefCount.ReferenceCount += f.Count(); + return accurateRefCount; + }); + + Connection.UpdateAll(incrementedFiles); + }); + } + + public void Dereference(params FileInfo[] files) + { + Connection.RunInTransaction(() => + { + var incrementedFiles = files.GroupBy(f => f.ID).Select(f => + { + var accurateRefCount = Connection.Get(f.First().ID); + accurateRefCount.ReferenceCount -= f.Count(); + return accurateRefCount; + }); + + Connection.UpdateAll(incrementedFiles); + }); + } + + private void deletePending() + { + Connection.RunInTransaction(() => + { + foreach (var f in Query(f => f.ReferenceCount < 1)) + { + try + { + Storage.Delete(Path.Combine(prefix, f.StoragePath)); + Connection.Delete(f); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete beatmap {f}"); + } + } + }); + } + } +} \ No newline at end of file diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs index 7dbc860979..e725742726 100644 --- a/osu.Game/IO/Serialization/IJsonSerializable.cs +++ b/osu.Game/IO/Serialization/IJsonSerializable.cs @@ -13,7 +13,7 @@ namespace osu.Game.IO.Serialization { public static string Serialize(this IJsonSerializable obj) { - return JsonConvert.SerializeObject(obj); + return JsonConvert.SerializeObject(obj, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); } public static T Deserialize(this string objString) diff --git a/osu.Game/IPC/BeatmapIPCChannel.cs b/osu.Game/IPC/BeatmapIPCChannel.cs index 61e6cc76cc..6a9019251c 100644 --- a/osu.Game/IPC/BeatmapIPCChannel.cs +++ b/osu.Game/IPC/BeatmapIPCChannel.cs @@ -4,15 +4,15 @@ using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Platform; -using osu.Game.Database; +using osu.Game.Beatmaps; namespace osu.Game.IPC { public class BeatmapIPCChannel : IpcChannel { - private readonly BeatmapDatabase beatmaps; + private readonly BeatmapManager beatmaps; - public BeatmapIPCChannel(IIpcHost host, BeatmapDatabase beatmaps = null) + public BeatmapIPCChannel(IIpcHost host, BeatmapManager beatmaps = null) : base(host) { this.beatmaps = beatmaps; diff --git a/osu.Game/IPC/ScoreIPCChannel.cs b/osu.Game/IPC/ScoreIPCChannel.cs index 7a509ee0e8..ae44250e8d 100644 --- a/osu.Game/IPC/ScoreIPCChannel.cs +++ b/osu.Game/IPC/ScoreIPCChannel.cs @@ -4,15 +4,15 @@ using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Platform; -using osu.Game.Database; +using osu.Game.Rulesets.Scoring; namespace osu.Game.IPC { public class ScoreIPCChannel : IpcChannel { - private readonly ScoreDatabase scores; + private readonly ScoreStore scores; - public ScoreIPCChannel(IIpcHost host, ScoreDatabase scores = null) + public ScoreIPCChannel(IIpcHost host, ScoreStore scores = null) : base(host) { this.scores = scores; diff --git a/osu.Game/Input/Bindings/DatabasedKeyBinding.cs b/osu.Game/Input/Bindings/DatabasedKeyBinding.cs new file mode 100644 index 0000000000..cbf74d6984 --- /dev/null +++ b/osu.Game/Input/Bindings/DatabasedKeyBinding.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets; +using SQLite.Net.Attributes; +using SQLiteNetExtensions.Attributes; + +namespace osu.Game.Input.Bindings +{ + [Table("KeyBinding")] + public class DatabasedKeyBinding : KeyBinding + { + [PrimaryKey, AutoIncrement] + public int ID { get; set; } + + [ForeignKey(typeof(RulesetInfo))] + public int? RulesetID { get; set; } + + [Indexed] + public int? Variant { get; set; } + + [Column("Keys")] + public string KeysString + { + get { return KeyCombination.ToString(); } + private set { KeyCombination = value; } + } + + [Indexed] + [Column("Action")] + public int IntAction + { + get { return (int)Action; } + set { Action = value; } + } + } +} \ No newline at end of file diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingInputManager.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingInputManager.cs new file mode 100644 index 0000000000..0a4fcf4389 --- /dev/null +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingInputManager.cs @@ -0,0 +1,54 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets; + +namespace osu.Game.Input.Bindings +{ + /// + /// A KeyBindingInputManager with a database backing for custom overrides. + /// + /// The type of the custom action. + public abstract class DatabasedKeyBindingInputManager : KeyBindingInputManager + where T : struct + { + private readonly RulesetInfo ruleset; + + private readonly int? variant; + + private KeyBindingStore store; + + public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(); + + /// + /// Create a new instance. + /// + /// A reference to identify the current . Used to lookup mappings. Null for global mappings. + /// An optional variant for the specified . Used when a ruleset has more than one possible keyboard layouts. + /// Specify how to deal with multiple matches of s and s. + protected DatabasedKeyBindingInputManager(RulesetInfo ruleset = null, int? variant = null, SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None) + : base(simultaneousMode) + { + this.ruleset = ruleset; + this.variant = variant; + + if (ruleset != null && variant == null) + throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided."); + } + + [BackgroundDependencyLoader] + private void load(KeyBindingStore keyBindings) + { + store = keyBindings; + } + + protected override void ReloadMappings() + { + KeyBindings = store.Query(ruleset?.ID, variant); + } + } +} \ No newline at end of file diff --git a/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs b/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs new file mode 100644 index 0000000000..759396e195 --- /dev/null +++ b/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; + +namespace osu.Game.Input.Bindings +{ + public class GlobalKeyBindingInputManager : DatabasedKeyBindingInputManager + { + private readonly Drawable handler; + + public GlobalKeyBindingInputManager(OsuGameBase game) + { + if (game is IKeyBindingHandler) + handler = game; + } + + public override IEnumerable DefaultKeyBindings => new[] + { + new KeyBinding(InputKey.F8, GlobalAction.ToggleChat), + new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial), + new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), + new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), + new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), + new KeyBinding(new[] { InputKey.Up }, GlobalAction.IncreaseVolume), + new KeyBinding(new[] { InputKey.MouseWheelUp }, GlobalAction.IncreaseVolume), + new KeyBinding(new[] { InputKey.Down }, GlobalAction.DecreaseVolume), + new KeyBinding(new[] { InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume), + }; + + protected override IEnumerable KeyBindingInputQueue => + handler == null ? base.KeyBindingInputQueue : new[] { handler }.Concat(base.KeyBindingInputQueue); + } + + public enum GlobalAction + { + [Description("Toggle chat overlay")] + ToggleChat, + [Description("Toggle social overlay")] + ToggleSocial, + [Description("Reset input settings")] + ResetInputSettings, + [Description("Toggle toolbar")] + ToggleToolbar, + [Description("Toggle settings")] + ToggleSettings, + [Description("Toggle osu!direct")] + ToggleDirect, + [Description("Increase Volume")] + IncreaseVolume, + [Description("Decrease Volume")] + DecreaseVolume, + } +} diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs new file mode 100644 index 0000000000..56302acafe --- /dev/null +++ b/osu.Game/Input/KeyBindingStore.cs @@ -0,0 +1,93 @@ +// 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.Input.Bindings; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using SQLite.Net; + +namespace osu.Game.Input +{ + public class KeyBindingStore : DatabaseBackedStore + { + public KeyBindingStore(SQLiteConnection connection, RulesetStore rulesets, Storage storage = null) + : base(connection, storage) + { + foreach (var info in rulesets.AllRulesets) + { + var ruleset = info.CreateInstance(); + foreach (var variant in ruleset.AvailableVariants) + insertDefaults(ruleset.GetDefaultKeyBindings(), info.ID, variant); + } + } + + public void Register(KeyBindingInputManager manager) => insertDefaults(manager.DefaultKeyBindings); + + protected override int StoreVersion => 3; + + protected override void PerformMigration(int currentVersion, int targetVersion) + { + base.PerformMigration(currentVersion, targetVersion); + + while (currentVersion++ < targetVersion) + { + switch (currentVersion) + { + case 1: + case 2: + case 3: + // cannot migrate; breaking underlying changes. + Reset(); + break; + } + } + } + + protected override void Prepare(bool reset = false) + { + if (reset) + Connection.DropTable(); + + Connection.CreateTable(); + } + + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) + { + var query = Query(rulesetId, variant); + + // compare counts in database vs defaults + foreach (var group in defaults.GroupBy(k => k.Action)) + { + int count; + while (group.Count() > (count = query.Count(k => (int)k.Action == (int)group.Key))) + { + var insertable = group.Skip(count).First(); + + // insert any defaults which are missing. + Connection.Insert(new DatabasedKeyBinding + { + KeyCombination = insertable.KeyCombination, + Action = insertable.Action, + RulesetID = rulesetId, + Variant = variant + }); + } + } + } + + protected override Type[] ValidTypes => new[] + { + typeof(DatabasedKeyBinding) + }; + + public IEnumerable Query(int? rulesetId = null, int? variant = null) => + Query(b => b.RulesetID == rulesetId && b.Variant == variant); + + public void Update(KeyBinding keyBinding) => Connection.Update(keyBinding); + } +} diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 7e3bb44465..57f5c54a18 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -117,7 +117,7 @@ namespace osu.Game.Online.API if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(Username, Password)) { //todo: this fails even on network-related issues. we should probably handle those differently. - //NotificationManager.ShowMessage("Login failed!"); + //NotificationOverlay.ShowMessage("Login failed!"); log.Add(@"Login failed!"); Password = null; continue; @@ -254,7 +254,7 @@ namespace osu.Game.Online.API { //OsuGame.Scheduler.Add(delegate { - //NotificationManager.ShowMessage($@"We just went {newState}!", newState == APIState.Online ? Color4.YellowGreen : Color4.OrangeRed, 5000); + //NotificationOverlay.ShowMessage($@"We just went {newState}!", newState == APIState.Online ? Color4.YellowGreen : Color4.OrangeRed, 5000); log.Add($@"We just went {newState}!"); Scheduler.Add(delegate { diff --git a/osu.Game/Online/API/OAuthToken.cs b/osu.Game/Online/API/OAuthToken.cs index 328888ab8a..1788adbf56 100644 --- a/osu.Game/Online/API/OAuthToken.cs +++ b/osu.Game/Online/API/OAuthToken.cs @@ -59,7 +59,6 @@ namespace osu.Game.Online.API { } - return null; } } diff --git a/osu.Game/Online/API/Requests/GetBeatmapDetailsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapDetailsRequest.cs index a529dde592..15e20a3d55 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapDetailsRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapDetailsRequest.cs @@ -2,7 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using Newtonsoft.Json; -using osu.Game.Database; +using osu.Game.Beatmaps; namespace osu.Game.Online.API.Requests { diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.cs new file mode 100644 index 0000000000..ca984d3511 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.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 System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Overlays.Direct; +using osu.Game.Rulesets; + +namespace osu.Game.Online.API.Requests +{ + public class GetBeatmapSetsRequest : APIRequest> + { + private readonly string query; + private readonly RulesetInfo ruleset; + private readonly RankStatus rankStatus; + private readonly DirectSortCriteria sortCriteria; + private readonly SortDirection direction; + private string directionString => direction == SortDirection.Descending ? @"desc" : @"asc"; + + public GetBeatmapSetsRequest(string query, RulesetInfo ruleset, RankStatus rankStatus = RankStatus.Any, DirectSortCriteria sortCriteria = DirectSortCriteria.Ranked, SortDirection direction = SortDirection.Descending) + { + this.query = System.Uri.EscapeDataString(query); + this.ruleset = ruleset; + this.rankStatus = rankStatus; + this.sortCriteria = sortCriteria; + this.direction = direction; + } + + protected override string Target => $@"beatmapsets/search?q={query}&m={ruleset.ID ?? 0}&s={(int)rankStatus}&sort={sortCriteria.ToString().ToLower()}_{directionString}"; + } + + public class GetBeatmapSetsResponse : BeatmapMetadata + { + [JsonProperty(@"covers")] + private BeatmapSetOnlineCovers covers { get; set; } + + [JsonProperty(@"previewUrl")] + private string preview { get; set; } + + [JsonProperty(@"play_count")] + private int playCount { get; set; } + + [JsonProperty(@"favourite_count")] + private int favouriteCount { get; set; } + + [JsonProperty(@"beatmaps")] + private IEnumerable beatmaps { get; set; } + + public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) + { + return new BeatmapSetInfo + { + Metadata = this, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = covers, + Preview = preview, + PlayCount = playCount, + FavouriteCount = favouriteCount, + }, + Beatmaps = beatmaps.Select(b => b.ToBeatmap(rulesets)).ToList(), + }; + } + + private class GetBeatmapSetsBeatmapResponse : BeatmapMetadata + { + [JsonProperty(@"playcount")] + private int playCount { get; set; } + + [JsonProperty(@"passcount")] + private int passCount { get; set; } + + [JsonProperty(@"mode_int")] + private int ruleset { get; set; } + + [JsonProperty(@"difficulty_rating")] + private double starDifficulty { get; set; } + + public BeatmapInfo ToBeatmap(RulesetStore rulesets) + { + return new BeatmapInfo + { + Metadata = this, + Ruleset = rulesets.GetRuleset(ruleset), + StarDifficulty = starDifficulty, + OnlineInfo = new BeatmapOnlineInfo + { + PlayCount = playCount, + PassCount = passCount, + }, + }; + } + } + } +} diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs index 5e6bf1ea9f..966049429e 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using osu.Framework.IO.Network; -using osu.Game.Database; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; namespace osu.Game.Online.API.Requests diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs index 2fd1ee5efc..2e3e7b01c8 100644 --- a/osu.Game/Online/API/Requests/GetUserRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRequest.cs @@ -7,9 +7,9 @@ namespace osu.Game.Online.API.Requests { public class GetUserRequest : APIRequest { - private int? userId; + private long? userId; - public GetUserRequest(int? userId = null) + public GetUserRequest(long? userId = null) { this.userId = userId; } diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs new file mode 100644 index 0000000000..5fb8606e1e --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using Newtonsoft.Json; +using osu.Game.Users; + +namespace osu.Game.Online.API.Requests +{ + public class GetUsersRequest : APIRequest> + { + protected override string Target => @"rankings/osu/performance"; + } + + public class RankingEntry + { + [JsonProperty] + public User User; + } +} diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 93fd0a8956..bec4aadb18 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Configuration; using osu.Framework.Lists; namespace osu.Game.Online.Chat @@ -25,7 +26,9 @@ namespace osu.Game.Online.Chat public readonly SortedList Messages = new SortedList(Comparer.Default); - //internal bool Joined; + private readonly List pendingMessages = new List(); + + public Bindable Joined = new Bindable(); public bool ReadOnly => Name != "#lazer"; @@ -37,6 +40,16 @@ namespace osu.Game.Online.Chat } public event Action> NewMessagesArrived; + public event Action PendingMessageResolved; + public event Action MessageRemoved; + + public void AddLocalEcho(LocalEchoMessage message) + { + pendingMessages.Add(message); + Messages.Add(message); + + NewMessagesArrived?.Invoke(new[] { message }); + } public void AddNewMessages(params Message[] messages) { @@ -51,11 +64,42 @@ namespace osu.Game.Online.Chat private void purgeOldMessages() { - int messageCount = Messages.Count; + // never purge local echos + int messageCount = Messages.Count - pendingMessages.Count; if (messageCount > MAX_HISTORY) Messages.RemoveRange(0, messageCount - MAX_HISTORY); } + /// + /// Replace or remove a message from the channel. + /// + /// The local echo message (client-side). + /// The response message, or null if the message became invalid. + public void ReplaceMessage(LocalEchoMessage echo, Message final) + { + if (!pendingMessages.Remove(echo)) + throw new InvalidOperationException("Attempted to remove echo that wasn't present"); + + Messages.Remove(echo); + + if (final == null) + { + MessageRemoved?.Invoke(echo); + return; + } + + if (Messages.Contains(final)) + { + // message already inserted, so let's throw away this update. + // we may want to handle this better in the future, but for the time being api requests are single-threaded so order is assumed. + MessageRemoved?.Invoke(echo); + return; + } + + Messages.Add(final); + PendingMessageResolved?.Invoke(echo, final); + } + public override string ToString() => Name; } } diff --git a/osu.Game/Online/Chat/LocalEchoMessage.cs b/osu.Game/Online/Chat/LocalEchoMessage.cs new file mode 100644 index 0000000000..079ec58686 --- /dev/null +++ b/osu.Game/Online/Chat/LocalEchoMessage.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Online.Chat +{ + public class LocalEchoMessage : Message + { + public LocalEchoMessage() : base(null) + { + } + } +} diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index 4c7e099647..509861868a 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -11,7 +11,7 @@ namespace osu.Game.Online.Chat public class Message : IComparable, IEquatable { [JsonProperty(@"message_id")] - public readonly long Id; + public readonly long? Id; //todo: this should be inside sender. [JsonProperty(@"sender_id")] @@ -37,14 +37,22 @@ namespace osu.Game.Online.Chat { } - public Message(long id) + public Message(long? id) { Id = id; } - public int CompareTo(Message other) => Id.CompareTo(other.Id); + public int CompareTo(Message other) + { + if (!Id.HasValue) + return other.Id.HasValue ? 1 : Timestamp.CompareTo(other.Timestamp); + if (!other.Id.HasValue) + return -1; - public bool Equals(Message other) => Id == other?.Id; + return Id.Value.CompareTo(other.Id.Value); + } + + public virtual bool Equals(Message other) => Id == other?.Id; public override int GetHashCode() => Id.GetHashCode(); } diff --git a/osu.Game/Online/Multiplayer/GameType.cs b/osu.Game/Online/Multiplayer/GameType.cs new file mode 100644 index 0000000000..c94b409d1b --- /dev/null +++ b/osu.Game/Online/Multiplayer/GameType.cs @@ -0,0 +1,146 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; + +namespace osu.Game.Online.Multiplayer +{ + public abstract class GameType + { + public abstract string Name { get; } + public abstract Drawable GetIcon(OsuColour colours, float size); + } + + public class GameTypeTag : GameType + { + public override string Name => "Tag"; + public override Drawable GetIcon(OsuColour colours, float size) + { + return new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.fa_refresh, + Size = new Vector2(size), + Colour = colours.Blue, + Shadow = false, + }; + } + } + + public class GameTypeVersus : GameType + { + public override string Name => "Versus"; + public override Drawable GetIcon(OsuColour colours, float size) + { + return new VersusRow(colours.Blue, colours.Blue, size * 0.6f) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + } + + public class GameTypeTagTeam : GameType + { + public override string Name => "Tag Team"; + public override Drawable GetIcon(OsuColour colours, float size) + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2f), + Children = new[] + { + new SpriteIcon + { + Icon = FontAwesome.fa_refresh, + Size = new Vector2(size * 0.75f), + Colour = colours.Blue, + Shadow = false, + }, + new SpriteIcon + { + Icon = FontAwesome.fa_refresh, + Size = new Vector2(size * 0.75f), + Colour = colours.Pink, + Shadow = false, + }, + }, + }; + } + } + + public class GameTypeTeamVersus : GameType + { + public override string Name => "Team Versus"; + public override Drawable GetIcon(OsuColour colours, float size) + { + return new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2f), + Children = new[] + { + new VersusRow(colours.Blue, colours.Pink, size * 0.5f), + new VersusRow(colours.Blue, colours.Pink, size * 0.5f), + }, + }; + } + } + + internal class VersusRow : FillFlowContainer + { + public VersusRow(Color4 first, Color4 second, float size) + { + var triangleSize = new Vector2(size); + AutoSizeAxes = Axes.Both; + Spacing = new Vector2(2f, 0f); + + Children = new[] + { + new Container + { + Size = triangleSize, + Colour = first, + Children = new[] + { + new EquilateralTriangle + { + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Rotation = 90, + EdgeSmoothness = new Vector2(1f), + }, + }, + }, + new Container + { + Size = triangleSize, + Colour = second, + Children = new[] + { + new EquilateralTriangle + { + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Rotation = -90, + EdgeSmoothness = new Vector2(1f), + }, + }, + }, + }; + } + } +} diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index c82025f902..e808f2c3ad 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -2,7 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Configuration; -using osu.Game.Database; +using osu.Game.Beatmaps; using osu.Game.Users; namespace osu.Game.Online.Multiplayer @@ -12,6 +12,9 @@ namespace osu.Game.Online.Multiplayer public Bindable Name = new Bindable(); public Bindable Host = new Bindable(); public Bindable Status = new Bindable(); - public Bindable Beatmap = new Bindable(); + public Bindable Type = new Bindable(); + public Bindable Beatmap = new Bindable(); + public Bindable MaxParticipants = new Bindable(); + public Bindable Participants = new Bindable(); } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2c952ee514..8c82071c23 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -8,28 +8,28 @@ using osu.Game.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; -using osu.Framework.Input; -using OpenTK.Input; using osu.Framework.Logging; using osu.Game.Graphics.UserInterface.Volume; using osu.Framework.Allocation; -using osu.Framework.Timing; using osu.Game.Overlays.Toolbar; using osu.Game.Screens; using osu.Game.Screens.Menu; using OpenTK; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; using osu.Framework.Threading; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Rulesets.Scoring; using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; using osu.Game.Screens.Play; +using osu.Game.Input.Bindings; namespace osu.Game { - public class OsuGame : OsuGameBase + public class OsuGame : OsuGameBase, IKeyBindingHandler { public Toolbar Toolbar; @@ -37,12 +37,18 @@ namespace osu.Game private MusicController musicController; - private NotificationManager notificationManager; + private NotificationOverlay notificationOverlay; private DialogOverlay dialogOverlay; private DirectOverlay direct; + private SocialOverlay social; + + private UserProfileOverlay userProfile; + + public virtual Storage GetStorageForStableInstall() => null; + private Intro intro { get @@ -54,6 +60,8 @@ namespace osu.Game } } + public float ToolbarOffset => Toolbar.Position.Y + Toolbar.DrawHeight; + private OsuScreen screenStack; private VolumeControl volume; @@ -74,9 +82,16 @@ namespace osu.Game public void ToggleDirect() => direct.ToggleVisibility(); + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => + dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + [BackgroundDependencyLoader] - private void load() + private void load(FrameworkConfigManager frameworkConfig) { + this.frameworkConfig = frameworkConfig; + if (!Host.IsPrimaryInstance) { Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); @@ -86,13 +101,13 @@ namespace osu.Game if (args?.Length > 0) { var paths = args.Where(a => !a.StartsWith(@"-")); - Task.Run(() => BeatmapDatabase.Import(paths.ToArray())); + Task.Run(() => BeatmapManager.Import(paths.ToArray())); } - Dependencies.Cache(this); + dependencies.Cache(this); configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); - Ruleset.Value = RulesetDatabase.GetRuleset(configRuleset.Value); + Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value); Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0; } @@ -113,14 +128,13 @@ namespace osu.Game if (!menu.IsCurrentScreen) { menu.MakeCurrent(); - Delay(500); - scoreLoad = Schedule(() => LoadScore(s)); + this.Delay(500).Schedule(() => LoadScore(s), out scoreLoad); return; } if (s.Beatmap == null) { - notificationManager.Post(new SimpleNotification + notificationOverlay.Post(new SimpleNotification { Text = @"Tried to load a score for a beatmap we don't have!", Icon = FontAwesome.fa_life_saver, @@ -128,7 +142,7 @@ namespace osu.Game return; } - Beatmap.Value = BeatmapDatabase.GetWorkingBeatmap(s.Beatmap); + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(s.Beatmap); menu.Push(new PlayerLoader(new ReplayPlayer(s.Replay))); } @@ -137,23 +151,23 @@ namespace osu.Game { base.LoadComplete(); - Add(new Drawable[] { + // hook up notifications to components. + BeatmapManager.PostNotification = n => notificationOverlay?.Post(n); + BeatmapManager.GetStableStorage = GetStorageForStableInstall; + + AddRange(new Drawable[] { new VolumeControlReceptor { RelativeSizeAxes = Axes.Both, - ActionRequested = delegate(InputState state) { volume.Adjust(state); } + ActionRequested = action => volume.Adjust(action) }, mainContent = new Container { RelativeSizeAxes = Axes.Both, }, volume = new VolumeControl(), - overlayContent = new Container{ RelativeSizeAxes = Axes.Both }, + overlayContent = new Container { RelativeSizeAxes = Axes.Both }, new OnScreenDisplay(), - new GlobalHotkeys //exists because UserInputManager is at a level below us. - { - Handler = globalHotkeyPressed - } }); LoadComponentAsync(screenStack = new Loader(), d => @@ -165,47 +179,59 @@ namespace osu.Game //overlay elements LoadComponentAsync(direct = new DirectOverlay { Depth = -1 }, mainContent.Add); + LoadComponentAsync(social = new SocialOverlay { Depth = -1 }, mainContent.Add); LoadComponentAsync(chat = new ChatOverlay { Depth = -1 }, mainContent.Add); - LoadComponentAsync(settings = new SettingsOverlay { Depth = -1 }, overlayContent.Add); + LoadComponentAsync(settings = new MainSettings + { + GetToolbarHeight = () => ToolbarOffset, + Depth = -1 + }, overlayContent.Add); + LoadComponentAsync(userProfile = new UserProfileOverlay { Depth = -2 }, mainContent.Add); LoadComponentAsync(musicController = new MusicController { - Depth = -2, + Depth = -3, Position = new Vector2(0, Toolbar.HEIGHT), Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }, overlayContent.Add); - LoadComponentAsync(notificationManager = new NotificationManager + LoadComponentAsync(notificationOverlay = new NotificationOverlay { - Depth = -2, + Depth = -3, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }, overlayContent.Add); LoadComponentAsync(dialogOverlay = new DialogOverlay { - Depth = -4, + Depth = -5, }, overlayContent.Add); Logger.NewEntry += entry => { if (entry.Level < LogLevel.Important) return; - notificationManager.Post(new SimpleNotification + notificationOverlay.Post(new SimpleNotification { Text = $@"{entry.Level}: {entry.Message}" }); }; - Dependencies.Cache(settings); - Dependencies.Cache(chat); - Dependencies.Cache(musicController); - Dependencies.Cache(notificationManager); - Dependencies.Cache(dialogOverlay); + dependencies.Cache(settings); + dependencies.Cache(social); + dependencies.Cache(chat); + dependencies.Cache(userProfile); + dependencies.Cache(musicController); + dependencies.Cache(notificationOverlay); + dependencies.Cache(dialogOverlay); + + // ensure both overlays aren't presented at the same time + chat.StateChanged += (container, state) => social.State = state == Visibility.Visible ? Visibility.Hidden : social.State; + social.StateChanged += (container, state) => chat.State = state == Visibility.Visible ? Visibility.Hidden : chat.State; LoadComponentAsync(Toolbar = new Toolbar { - Depth = -3, + Depth = -4, OnHome = delegate { intro?.ChildScreen?.MakeCurrent(); }, }, overlayContent.Add); @@ -214,10 +240,10 @@ namespace osu.Game switch (settings.State) { case Visibility.Hidden: - intro.MoveToX(0, SettingsOverlay.TRANSITION_LENGTH, EasingTypes.OutQuint); + intro.MoveToX(0, SettingsOverlay.TRANSITION_LENGTH, Easing.OutQuint); break; case Visibility.Visible: - intro.MoveToX(SettingsOverlay.SIDEBAR_WIDTH / 2, SettingsOverlay.TRANSITION_LENGTH, EasingTypes.OutQuint); + intro.MoveToX(SettingsOverlay.SIDEBAR_WIDTH / 2, SettingsOverlay.TRANSITION_LENGTH, Easing.OutQuint); break; } }; @@ -225,47 +251,43 @@ namespace osu.Game Cursor.State = Visibility.Hidden; } - private bool globalHotkeyPressed(InputState state, KeyDownEventArgs args) + public bool OnPressed(GlobalAction action) { - if (args.Repeat || intro == null) return false; + if (intro == null) return false; - switch (args.Key) + switch (action) { - case Key.F8: + case GlobalAction.ToggleChat: chat.ToggleVisibility(); return true; - case Key.PageUp: - case Key.PageDown: - var swClock = (Clock as ThrottledFrameClock)?.Source as StopwatchClock; - if (swClock == null) return false; - - swClock.Rate *= args.Key == Key.PageUp ? 1.1f : 0.9f; - Logger.Log($@"Adjusting game clock to {swClock.Rate}", LoggingTarget.Debug); + case GlobalAction.ToggleSocial: + social.ToggleVisibility(); return true; - } + case GlobalAction.ResetInputSettings: + var sensitivity = frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity); - if (state.Keyboard.ControlPressed) - { - switch (args.Key) - { - case Key.T: - Toolbar.ToggleVisibility(); - return true; - case Key.O: - settings.ToggleVisibility(); - return true; - case Key.D: - if (state.Keyboard.ShiftPressed || state.Keyboard.AltPressed) - return false; + sensitivity.Disabled = false; + sensitivity.Value = 1; + sensitivity.Disabled = true; - direct.ToggleVisibility(); - return true; - } + frameworkConfig.Set(FrameworkSetting.ActiveInputHandlers, string.Empty); + return true; + case GlobalAction.ToggleToolbar: + Toolbar.ToggleVisibility(); + return true; + case GlobalAction.ToggleSettings: + settings.ToggleVisibility(); + return true; + case GlobalAction.ToggleDirect: + direct.ToggleVisibility(); + return true; } return false; } + public bool OnReleased(GlobalAction action) => false; + public event Action ScreenChanged; private Container mainContent; @@ -273,6 +295,7 @@ namespace osu.Game private Container overlayContent; private OsuScreen currentScreen; + private FrameworkConfigManager frameworkConfig; private void screenChanged(Screen newScreen) { @@ -292,6 +315,9 @@ namespace osu.Game musicController.State = Visibility.Hidden; chat.State = Visibility.Hidden; direct.State = Visibility.Hidden; + social.State = Visibility.Hidden; + userProfile.State = Visibility.Hidden; + notificationOverlay.State = Visibility.Hidden; } else { @@ -332,7 +358,14 @@ namespace osu.Game { base.UpdateAfterChildren(); - mainContent.Padding = new MarginPadding { Top = Toolbar.Position.Y + Toolbar.DrawHeight }; + // we only want to apply these restrictions when we are inside a screen stack. + // the use case for not applying is in visual/unit tests. + bool applyRestrictions = !currentScreen?.AllowBeatmapRulesetChange ?? false; + + Ruleset.Disabled = applyRestrictions; + Beatmap.Disabled = applyRestrictions; + + mainContent.Padding = new MarginPadding { Top = ToolbarOffset }; Cursor.State = currentScreen?.HasLocalCursorDisplayed == false ? Visibility.Visible : Visibility.Hidden; } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b228b6485a..952dc900a2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -11,15 +11,19 @@ using osu.Framework.Graphics.Containers; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.IO; using osu.Game.Configuration; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Processing; using osu.Game.Online.API; using SQLite.Net; using osu.Framework.Graphics.Performance; +using osu.Game.Database; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Scoring; namespace osu.Game { @@ -27,23 +31,27 @@ namespace osu.Game { protected OsuConfigManager LocalConfig; - protected BeatmapDatabase BeatmapDatabase; + protected BeatmapManager BeatmapManager; - protected RulesetDatabase RulesetDatabase; + protected RulesetStore RulesetStore; - protected ScoreDatabase ScoreDatabase; + protected FileStore FileStore; + + protected ScoreStore ScoreStore; + + protected KeyBindingStore KeyBindingStore; protected override string MainResourceFile => @"osu.Game.Resources.dll"; public APIAccess API; - protected override Container Content => ratioContainer; + private Container content; - private RatioAdjust ratioContainer; + protected override Container Content => content; protected MenuCursor Cursor; - public readonly Bindable Beatmap = new Bindable(); + public Bindable Beatmap { get; private set; } private Bindable fpsDisplayVisible; @@ -81,21 +89,32 @@ namespace osu.Game Name = @"osu!lazer"; } + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => + dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + + private SQLiteConnection connection; + [BackgroundDependencyLoader] private void load() { - Dependencies.Cache(this); - Dependencies.Cache(LocalConfig); + dependencies.Cache(this); + dependencies.Cache(LocalConfig); - SQLiteConnection connection = Host.Storage.GetDatabase(@"client"); + connection = Host.Storage.GetDatabase(@"client"); - Dependencies.Cache(RulesetDatabase = new RulesetDatabase(Host.Storage, connection)); - Dependencies.Cache(BeatmapDatabase = new BeatmapDatabase(Host.Storage, connection, RulesetDatabase, Host)); - Dependencies.Cache(ScoreDatabase = new ScoreDatabase(Host.Storage, connection, Host, BeatmapDatabase)); - Dependencies.Cache(new OsuColour()); + connection.CreateTable(); + + dependencies.Cache(RulesetStore = new RulesetStore(connection)); + dependencies.Cache(FileStore = new FileStore(connection, Host.Storage)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, FileStore, connection, RulesetStore, Host)); + dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, connection, Host, BeatmapManager, RulesetStore)); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(connection, RulesetStore)); + dependencies.Cache(new OsuColour()); //this completely overrides the framework default. will need to change once we make a proper FontStore. - Dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 }, true); + dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 }, true); Fonts.AddStore(new GlyphStore(Resources, @"Fonts/FontAwesome")); Fonts.AddStore(new GlyphStore(Resources, @"Fonts/osuFont")); @@ -121,17 +140,37 @@ namespace osu.Game Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera")); Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera-Light")); - OszArchiveReader.Register(); + var defaultBeatmap = new DummyWorkingBeatmap(this); + Beatmap = new NonNullableBindable(defaultBeatmap); + BeatmapManager.DefaultBeatmap = defaultBeatmap; - Dependencies.Cache(API = new APIAccess + dependencies.Cache(API = new APIAccess { Username = LocalConfig.Get(OsuSetting.Username), Token = LocalConfig.Get(OsuSetting.Token) }); + Beatmap.ValueChanged += b => + { + // compare to last baetmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) + if (lastBeatmap?.Track != b.Track) + { + // this disposal is done to stop the audio track. + // it may not be exactly what we want for cases beatmaps are reused, as it will + // trigger a fresh load of contained resources. + lastBeatmap?.Dispose(); + + Audio.Track.AddItem(b.Track); + } + + lastBeatmap = b; + }; + API.Register(this); } + private WorkingBeatmap lastBeatmap; + public void APIStateChanged(APIAccess api, APIState state) { switch (state) @@ -146,24 +185,28 @@ namespace osu.Game { base.LoadComplete(); - base.Content.Add(ratioContainer = new RatioAdjust + GlobalKeyBindingInputManager globalBinding; + + base.Content.Add(new RatioAdjust { Children = new Drawable[] { - new Container + Cursor = new MenuCursor(), + globalBinding = new GlobalKeyBindingInputManager(this) { - AlwaysReceiveInput = true, RelativeSizeAxes = Axes.Both, - Depth = float.MinValue, - Children = new Drawable[] + Child = new OsuTooltipContainer(Cursor) { - Cursor = new MenuCursor(), - new OsuTooltipContainer(Cursor) { Depth = -1 }, + RelativeSizeAxes = Axes.Both, + Child = content = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both }, } - }, + } } }); + KeyBindingStore.Register(globalBinding); + dependencies.Cache(globalBinding); + // TODO: This is temporary until we reimplement the local FPS display. // It's just to allow end-users to access the framework FPS display without knowing the shortcut key. fpsDisplayVisible = LocalConfig.GetBindable(OsuSetting.ShowFpsDisplay); @@ -196,6 +239,8 @@ namespace osu.Game LocalConfig.Save(); } + connection.Dispose(); + base.Dispose(isDisposing); } } diff --git a/osu.Game/Overlays/Chat/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelListItem.cs new file mode 100644 index 0000000000..f43154ea20 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelListItem.cs @@ -0,0 +1,189 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Overlays.Chat +{ + public class ChannelListItem : OsuClickableContainer, IFilterable + { + private const float width_padding = 5; + private const float channel_width = 150; + private const float text_size = 15; + private const float transition_duration = 100; + + private readonly Channel channel; + + private readonly Bindable joinedBind = new Bindable(); + private readonly OsuSpriteText name; + private readonly OsuSpriteText topic; + private readonly SpriteIcon joinedCheckmark; + + private Color4 joinedColour; + private Color4 topicColour; + private Color4 hoverColour; + + public string[] FilterTerms => new[] { channel.Name }; + public bool MatchingFilter + { + set + { + this.FadeTo(value ? 1f : 0f, 100); + } + } + + public Action OnRequestJoin; + public Action OnRequestLeave; + + public ChannelListItem(Channel channel) + { + this.channel = channel; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Action = () => { (channel.Joined ? OnRequestLeave : OnRequestJoin)?.Invoke(channel); }; + + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new Container + { + Children = new[] + { + joinedCheckmark = new SpriteIcon + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Icon = FontAwesome.fa_check_circle, + Size = new Vector2(text_size), + Shadow = false, + Margin = new MarginPadding { Right = 10f }, + Alpha = 0f, + }, + }, + }, + new Container + { + Width = channel_width, + AutoSizeAxes = Axes.Y, + Children = new[] + { + name = new OsuSpriteText + { + Text = channel.ToString(), + TextSize = text_size, + Font = @"Exo2.0-Bold", + Shadow = false, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + Width = 0.7f, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Left = width_padding }, + Children = new[] + { + topic = new OsuSpriteText + { + Text = channel.Topic, + TextSize = text_size, + Font = @"Exo2.0-SemiBold", + Shadow = false, + Alpha = 0.8f, + }, + }, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Left = width_padding }, + Spacing = new Vector2(3f, 0f), + Children = new Drawable[] + { + new SpriteIcon + { + Icon = FontAwesome.fa_user, + Size = new Vector2(text_size - 2), + Shadow = false, + Margin = new MarginPadding { Top = 1 }, + }, + new OsuSpriteText + { + Text = @"0", + TextSize = text_size, + Font = @"Exo2.0-SemiBold", + Shadow = false, + }, + }, + }, + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + topicColour = colours.Gray9; + joinedColour = colours.Blue; + hoverColour = colours.Yellow; + + joinedBind.ValueChanged += updateColour; + joinedBind.BindTo(channel.Joined); + } + + protected override bool OnHover(InputState state) + { + if (!channel.Joined.Value) + name.FadeColour(hoverColour, 50, Easing.OutQuint); + + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + if (!channel.Joined.Value) + name.FadeColour(Color4.White, transition_duration); + } + + private void updateColour(bool joined) + { + if (joined) + { + name.FadeColour(Color4.White, transition_duration); + joinedCheckmark.FadeTo(1f, transition_duration); + topic.FadeTo(0.8f, transition_duration); + topic.FadeColour(Color4.White, transition_duration); + this.FadeColour(joinedColour, transition_duration); + } + else + { + joinedCheckmark.FadeTo(0f, transition_duration); + topic.FadeTo(1f, transition_duration); + topic.FadeColour(topicColour, transition_duration); + this.FadeColour(Color4.White, transition_duration); + } + } + } +} diff --git a/osu.Game/Overlays/Chat/ChannelSection.cs b/osu.Game/Overlays/Chat/ChannelSection.cs new file mode 100644 index 0000000000..1f046aff2a --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelSection.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; + +namespace osu.Game.Overlays.Chat +{ + public class ChannelSection : Container, IHasFilterableChildren + { + private readonly OsuSpriteText header; + + public readonly FillFlowContainer ChannelFlow; + + public IEnumerable FilterableChildren => ChannelFlow.Children; + public string[] FilterTerms => new[] { Header }; + public bool MatchingFilter + { + set + { + this.FadeTo(value ? 1f : 0f, 100); + } + } + + public string Header + { + get { return header.Text; } + set { header.Text = value.ToUpper(); } + } + + public IEnumerable Channels + { + set { ChannelFlow.ChildrenEnumerable = value.Select(c => new ChannelListItem(c)); } + } + + public ChannelSection() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Children = new Drawable[] + { + header = new OsuSpriteText + { + TextSize = 15, + Font = @"Exo2.0-Bold", + }, + ChannelFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 25 }, + Spacing = new Vector2(0f, 5f), + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Chat/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/ChannelSelectionOverlay.cs new file mode 100644 index 0000000000..4362b3f787 --- /dev/null +++ b/osu.Game/Overlays/Chat/ChannelSelectionOverlay.cs @@ -0,0 +1,185 @@ +// 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 OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Chat; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Overlays.Chat +{ + public class ChannelSelectionOverlay : OsuFocusedOverlayContainer + { + public static readonly float WIDTH_PADDING = 170; + + private const float transition_duration = 500; + + private readonly Box bg; + private readonly Triangles triangles; + private readonly Box headerBg; + private readonly SearchTextBox search; + private readonly SearchContainer sectionsFlow; + + public Action OnRequestJoin; + public Action OnRequestLeave; + + public IEnumerable Sections + { + set + { + sectionsFlow.ChildrenEnumerable = value; + + foreach (ChannelSection s in sectionsFlow.Children) + { + foreach (ChannelListItem c in s.ChannelFlow.Children) + { + c.OnRequestJoin = channel => { OnRequestJoin?.Invoke(channel); }; + c.OnRequestLeave = channel => { OnRequestLeave?.Invoke(channel); }; + } + } + } + } + + public ChannelSelectionOverlay() + { + RelativeSizeAxes = Axes.X; + + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + bg = new Box + { + RelativeSizeAxes = Axes.Both, + }, + triangles = new Triangles + { + RelativeSizeAxes = Axes.Both, + TriangleScale = 5, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 85, Right = WIDTH_PADDING }, + Children = new[] + { + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + sectionsFlow = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + LayoutDuration = 200, + LayoutEasing = Easing.OutQuint, + Spacing = new Vector2(0f, 20f), + Padding = new MarginPadding { Vertical = 20, Left = WIDTH_PADDING }, + }, + }, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + headerBg = new Box + { + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 10f), + Padding = new MarginPadding { Top = 10f, Bottom = 10f, Left = WIDTH_PADDING, Right = WIDTH_PADDING }, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = @"Chat Channels", + TextSize = 20, + Shadow = false, + }, + search = new HeaderSearchTextBox + { + RelativeSizeAxes = Axes.X, + PlaceholderText = @"Search", + Exit = Hide, + }, + }, + }, + }, + }, + }; + + search.Current.ValueChanged += newValue => sectionsFlow.SearchTerm = newValue; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + bg.Colour = colours.Gray3; + triangles.ColourDark = colours.Gray3; + triangles.ColourLight = OsuColour.FromHex(@"353535"); + + headerBg.Colour = colours.Gray2.Opacity(0.75f); + } + + protected override void OnFocus(InputState state) + { + GetContainingInputManager().ChangeFocus(search); + base.OnFocus(state); + } + + protected override void PopIn() + { + if (Alpha == 0) this.MoveToY(DrawHeight); + + this.FadeIn(transition_duration, Easing.OutQuint); + this.MoveToY(0, transition_duration, Easing.OutQuint); + + search.HoldFocus = true; + base.PopIn(); + } + + protected override void PopOut() + { + this.FadeOut(transition_duration, Easing.InSine); + this.MoveToY(DrawHeight, transition_duration, Easing.InSine); + + search.HoldFocus = false; + base.PopOut(); + } + + private class HeaderSearchTextBox : SearchTextBox + { + protected override Color4 BackgroundFocused => Color4.Black.Opacity(0.2f); + protected override Color4 BackgroundUnfocused => Color4.Black.Opacity(0.2f); + } + } +} diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 3aaca7593c..afc6589f57 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -1,21 +1,25 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -using OpenTK; -using OpenTK.Graphics; +using osu.Game.Users; namespace osu.Game.Overlays.Chat { public class ChatLine : Container { - public readonly Message Message; - - private static readonly Color4[] username_colours = { + private static readonly Color4[] username_colours = + { OsuColour.FromHex("588c7e"), OsuColour.FromHex("b2a367"), OsuColour.FromHex("c98f65"), @@ -53,21 +57,18 @@ namespace osu.Game.Overlays.Chat OsuColour.FromHex("992861"), }; - private Color4 getUsernameColour(Message message) - { - if (!string.IsNullOrEmpty(message.Sender?.Colour)) - return OsuColour.FromHex(message.Sender.Colour); - - //todo: use User instead of Message when user_id is correctly populated. - return username_colours[message.UserId % username_colours.Length]; - } - public const float LEFT_PADDING = message_padding + padding * 2; private const float padding = 15; private const float message_padding = 200; private const float text_size = 20; + private Action loadProfile; + + private Color4 customUsernameColour; + + private OsuSpriteText timestamp; + public ChatLine(Message message) { Message = message; @@ -76,6 +77,84 @@ namespace osu.Game.Overlays.Chat AutoSizeAxes = Axes.Y; Padding = new MarginPadding { Left = padding, Right = padding }; + } + + private Message message; + private OsuSpriteText username; + private OsuTextFlowContainer contentFlow; + + public Message Message + { + get { return message; } + set + { + if (message == value) return; + + message = value; + + if (!IsLoaded) + return; + + updateMessageContent(); + } + } + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, UserProfileOverlay profile) + { + customUsernameColour = colours.ChatBlue; + loadProfile = u => profile?.ShowUser(u); + } + + private bool senderHasBackground => !string.IsNullOrEmpty(message.Sender.Colour); + + protected override void LoadComplete() + { + base.LoadComplete(); + + bool hasBackground = senderHasBackground; + + Drawable effectedUsername = username = new OsuSpriteText + { + Font = @"Exo2.0-BoldItalic", + Colour = hasBackground ? customUsernameColour : username_colours[message.Sender.Id % username_colours.Length], + TextSize = text_size, + }; + + if (hasBackground) + { + // Background effect + effectedUsername = new Container + { + AutoSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 4, + EdgeEffect = new EdgeEffectParameters + { + Roundness = 1, + Offset = new Vector2(0, 3), + Radius = 3, + Colour = Color4.Black.Opacity(0.3f), + Type = EdgeEffectType.Shadow, + }, + // Drop shadow effect + Child = new Container + { + AutoSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 4, + EdgeEffect = new EdgeEffectParameters + { + Radius = 1, + Colour = OsuColour.FromHex(message.Sender.Colour), + Type = EdgeEffectType.Shadow, + }, + Padding = new MarginPadding { Left = 3, Right = 3, Bottom = 1, Top = -3 }, + Y = 3, + Child = username, + } + }; + } Children = new Drawable[] { @@ -84,25 +163,22 @@ namespace osu.Game.Overlays.Chat Size = new Vector2(message_padding, text_size), Children = new Drawable[] { - new OsuSpriteText + timestamp = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = @"Exo2.0-SemiBold", - Text = $@"{Message.Timestamp.LocalDateTime:HH:mm:ss}", FixedWidth = true, TextSize = text_size * 0.75f, - Alpha = 0.4f, }, - new OsuSpriteText + new ClickableContainer { - Font = @"Exo2.0-BoldItalic", - Text = $@"{Message.Sender.Username}:", - Colour = getUsernameColour(Message), - TextSize = text_size, + AutoSizeAxes = Axes.Both, Origin = Anchor.TopRight, Anchor = Anchor.TopRight, - } + Child = effectedUsername, + Action = () => loadProfile(message.Sender), + }, } }, new Container @@ -112,16 +188,27 @@ namespace osu.Game.Overlays.Chat Padding = new MarginPadding { Left = message_padding + padding }, Children = new Drawable[] { - new OsuSpriteText + contentFlow = new OsuTextFlowContainer(t => { t.TextSize = text_size; }) { - Text = Message.Content, - TextSize = text_size, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, } } } }; + + updateMessageContent(); + FinishTransforms(true); + } + + private void updateMessageContent() + { + this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint); + timestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint); + + timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"; + username.Text = $@"{message.Sender.Username}" + (senderHasBackground ? "" : ":"); + contentFlow.Text = message.Content; } } } diff --git a/osu.Game/Overlays/Chat/ChatTabControl.cs b/osu.Game/Overlays/Chat/ChatTabControl.cs index a281cff7db..4ff9169877 100644 --- a/osu.Game/Overlays/Chat/ChatTabControl.cs +++ b/osu.Game/Overlays/Chat/ChatTabControl.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; @@ -26,22 +27,39 @@ namespace osu.Game.Overlays.Chat public readonly Bindable ChannelSelectorActive = new Bindable(); + private readonly ChannelTabItem.ChannelSelectorTabItem selectorTab; + public ChatTabControl() { TabContainer.Margin = new MarginPadding { Left = 50 }; TabContainer.Spacing = new Vector2(-shear_width, 0); TabContainer.Masking = false; - AddInternal(new TextAwesome + AddInternal(new SpriteIcon { Icon = FontAwesome.fa_comments, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - TextSize = 20, - Padding = new MarginPadding(10), + Size = new Vector2(20), + Margin = new MarginPadding(10), }); - AddTabItem(new ChannelTabItem.ChannelSelectorTabItem(new Channel { Name = "+" }, ChannelSelectorActive)); + AddTabItem(selectorTab = new ChannelTabItem.ChannelSelectorTabItem(new Channel { Name = "+" })); + + ChannelSelectorActive.BindTo(selectorTab.Active); + } + + protected override void SelectTab(TabItem tab) + { + if (tab is ChannelTabItem.ChannelSelectorTabItem) + { + tab.Active.Toggle(); + return; + } + + selectorTab.Active.Value = false; + + base.SelectTab(tab); } private class ChannelTabItem : TabItem @@ -54,19 +72,7 @@ namespace osu.Game.Overlays.Chat private readonly SpriteText textBold; private readonly Box box; private readonly Box highlightBox; - private readonly TextAwesome icon; - - public override bool Active - { - get { return base.Active; } - set - { - if (Active == value) return; - - base.Active = value; - updateState(); - } - } + private readonly SpriteIcon icon; private void updateState() { @@ -80,30 +86,30 @@ namespace osu.Game.Overlays.Chat private void fadeActive() { - ResizeTo(new Vector2(Width, 1.1f), transition_length, EasingTypes.OutQuint); + this.ResizeTo(new Vector2(Width, 1.1f), transition_length, Easing.OutQuint); - box.FadeColour(backgroundActive, transition_length, EasingTypes.OutQuint); - highlightBox.FadeIn(transition_length, EasingTypes.OutQuint); + box.FadeColour(backgroundActive, transition_length, Easing.OutQuint); + highlightBox.FadeIn(transition_length, Easing.OutQuint); - text.FadeOut(transition_length, EasingTypes.OutQuint); - textBold.FadeIn(transition_length, EasingTypes.OutQuint); + text.FadeOut(transition_length, Easing.OutQuint); + textBold.FadeIn(transition_length, Easing.OutQuint); } private void fadeInactive() { - ResizeTo(new Vector2(Width, 1), transition_length, EasingTypes.OutQuint); + this.ResizeTo(new Vector2(Width, 1), transition_length, Easing.OutQuint); - box.FadeColour(backgroundInactive, transition_length, EasingTypes.OutQuint); - highlightBox.FadeOut(transition_length, EasingTypes.OutQuint); + box.FadeColour(backgroundInactive, transition_length, Easing.OutQuint); + highlightBox.FadeOut(transition_length, Easing.OutQuint); - text.FadeIn(transition_length, EasingTypes.OutQuint); - textBold.FadeOut(transition_length, EasingTypes.OutQuint); + text.FadeIn(transition_length, Easing.OutQuint); + textBold.FadeOut(transition_length, Easing.OutQuint); } protected override bool OnHover(InputState state) { if (!Active) - box.FadeColour(backgroundHover, transition_length, EasingTypes.OutQuint); + box.FadeColour(backgroundHover, transition_length, Easing.OutQuint); return true; } @@ -141,7 +147,7 @@ namespace osu.Game.Overlays.Chat Shear = new Vector2(shear_width / ChatOverlay.TAB_AREA_HEIGHT, 0); Masking = true; - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Radius = 10, @@ -170,7 +176,7 @@ namespace osu.Game.Overlays.Chat RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - icon = new TextAwesome + icon = new SpriteIcon { Icon = FontAwesome.fa_hashtag, Anchor = Anchor.CentreLeft, @@ -178,7 +184,7 @@ namespace osu.Game.Overlays.Chat Colour = Color4.Black, X = -10, Alpha = 0.2f, - TextSize = ChatOverlay.TAB_AREA_HEIGHT, + Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT), }, text = new OsuSpriteText { @@ -205,21 +211,8 @@ namespace osu.Game.Overlays.Chat public class ChannelSelectorTabItem : ChannelTabItem { - public override bool Active + public ChannelSelectorTabItem(Channel value) : base(value) { - get { return base.Active; } - set - { - activeBindable.Value = value; - base.Active = value; - } - } - - private readonly Bindable activeBindable; - - public ChannelSelectorTabItem(Channel value, Bindable active) : base(value) - { - activeBindable = active; Depth = float.MaxValue; Width = 45; @@ -236,6 +229,10 @@ namespace osu.Game.Overlays.Chat backgroundActive = colour.Gray3; } } + + protected override void OnActivated() => updateState(); + + protected override void OnDeactivated() => updateState(); } } } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 50c849f00e..840edaece2 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -3,17 +3,32 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using OpenTK.Graphics; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; using osu.Game.Online.Chat; namespace osu.Game.Overlays.Chat { public class DrawableChannel : Container { + private class ChatLineContainer : FillFlowContainer + { + protected override int Compare(Drawable x, Drawable y) + { + var xC = (ChatLine)x; + var yC = (ChatLine)y; + + return xC.Message.CompareTo(yC.Message); + } + } + public readonly Channel Channel; - private readonly FillFlowContainer flow; + private readonly ChatLineContainer flow; private readonly ScrollContainer scroll; public DrawableChannel(Channel channel) @@ -24,47 +39,56 @@ namespace osu.Game.Overlays.Chat Children = new Drawable[] { - scroll = new ScrollContainer + scroll = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + // Some chat lines have effects that slightly protrude to the bottom, + // which we do not want to mask away, hence the padding. + Padding = new MarginPadding { Bottom = 5 }, + Child = flow = new ChatLineContainer { - flow = new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 20, Right = 20 } - } - } + Padding = new MarginPadding { Left = 20, Right = 20 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }, } }; - channel.NewMessagesArrived += newMessagesArrived; + Channel.NewMessagesArrived += newMessagesArrived; + Channel.MessageRemoved += messageRemoved; + Channel.PendingMessageResolved += pendingMessageResolved; + } + + [BackgroundDependencyLoader] + private void load() + { + newMessagesArrived(Channel.Messages); } protected override void LoadComplete() { base.LoadComplete(); - - newMessagesArrived(Channel.Messages); scrollToEnd(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + Channel.NewMessagesArrived -= newMessagesArrived; + Channel.MessageRemoved -= messageRemoved; + Channel.PendingMessageResolved -= pendingMessageResolved; } private void newMessagesArrived(IEnumerable newMessages) { - if (!IsLoaded) return; - + // Add up to last Channel.MAX_HISTORY messages var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY)); - //up to last Channel.MAX_HISTORY messages - flow.Add(displayMessages.Select(m => new ChatLine(m))); + flow.AddRange(displayMessages.Select(m => new ChatLine(m))); + + if (!IsLoaded) return; if (scroll.IsScrolledToEnd(10) || !flow.Children.Any()) scrollToEnd(); @@ -81,6 +105,24 @@ namespace osu.Game.Overlays.Chat } } - private void scrollToEnd() => Scheduler.AddDelayed(() => scroll.ScrollToEnd(), 50); + private void pendingMessageResolved(Message existing, Message updated) + { + var found = flow.Children.LastOrDefault(c => c.Message == existing); + if (found != null) + { + Trace.Assert(updated.Id.HasValue, "An updated message was returned with no ID."); + + flow.Remove(found); + found.Message = updated; + flow.Add(found); + } + } + + private void messageRemoved(Message removed) + { + flow.Children.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); + } + + private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd()); } } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index a9970e5e95..6dd5425fd1 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -6,35 +6,38 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using OpenTK; +using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Threading; -using osu.Game.Graphics.Sprites; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; -using osu.Game.Graphics.UserInterface; -using osu.Framework.Graphics.UserInterface; -using OpenTK.Graphics; -using osu.Framework.Input; -using osu.Game.Configuration; -using osu.Game.Graphics; using osu.Game.Overlays.Chat; namespace osu.Game.Overlays { - public class ChatOverlay : FocusedOverlayContainer, IOnlineComponent + public class ChatOverlay : OsuFocusedOverlayContainer, IOnlineComponent { private const float textbox_height = 60; + private const float channel_selection_min_height = 0.3f; private ScheduledDelegate messageRequest; - private readonly Container currentChannelContainer; + private readonly Container currentChannelContainer; - private readonly FocusedTextBox inputTextBox; + private readonly LoadingAnimation loading; + + private readonly FocusedTextBox textbox; private APIAccess api; @@ -48,16 +51,22 @@ namespace osu.Game.Overlays private readonly ChatTabControl channelTabs; + private readonly Container chatContainer; + private readonly Container tabsArea; private readonly Box chatBackground; private readonly Box tabBackground; private Bindable chatHeight; + private readonly Container channelSelectionContainer; + private readonly ChannelSelectionOverlay channelSelection; + + public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceiveMouseInputAt(screenSpacePos) || channelSelection.State == Visibility.Visible && channelSelection.ReceiveMouseInputAt(screenSpacePos); + public ChatOverlay() { RelativeSizeAxes = Axes.Both; RelativePositionAxes = Axes.Both; - Size = new Vector2(1, DEFAULT_HEIGHT); Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; @@ -65,81 +74,131 @@ namespace osu.Game.Overlays Children = new Drawable[] { - new Container + channelSelectionContainer = new Container { - Name = @"chat area", RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = TAB_AREA_HEIGHT }, - Children = new Drawable[] + Height = 1f - DEFAULT_HEIGHT, + Masking = true, + Children = new[] { - chatBackground = new Box + channelSelection = new ChannelSelectionOverlay { RelativeSizeAxes = Axes.Both, }, - currentChannelContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Bottom = textbox_height + padding - }, - }, + }, + }, + chatContainer = new Container + { + Name = @"chat container", + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Height = DEFAULT_HEIGHT, + Children = new[] + { new Container { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = textbox_height, - Padding = new MarginPadding - { - Top = padding * 2, - Bottom = padding * 2, - Left = ChatLine.LEFT_PADDING + padding * 2, - Right = padding * 2, - }, + Name = @"chat area", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = TAB_AREA_HEIGHT }, Children = new Drawable[] { - inputTextBox = new FocusedTextBox + chatBackground = new Box { RelativeSizeAxes = Axes.Both, - Height = 1, - PlaceholderText = "type your message", - Exit = () => State = Visibility.Hidden, - OnCommit = postMessage, - HoldFocus = true, - } + }, + currentChannelContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Bottom = textbox_height + }, + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = textbox_height, + Padding = new MarginPadding + { + Top = padding * 2, + Bottom = padding * 2, + Left = ChatLine.LEFT_PADDING + padding * 2, + Right = padding * 2, + }, + Children = new Drawable[] + { + textbox = new FocusedTextBox + { + RelativeSizeAxes = Axes.Both, + Height = 1, + PlaceholderText = "type your message", + Exit = () => State = Visibility.Hidden, + OnCommit = postMessage, + ReleaseFocusOnCommit = false, + HoldFocus = true, + } + } + }, + loading = new LoadingAnimation(), } - } - } - }, - new Container - { - Name = @"tabs area", - RelativeSizeAxes = Axes.X, - Height = TAB_AREA_HEIGHT, - Children = new Drawable[] - { - tabBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, }, - channelTabs = new ChatTabControl + tabsArea = new Container { - RelativeSizeAxes = Axes.Both, + Name = @"tabs area", + RelativeSizeAxes = Axes.X, + Height = TAB_AREA_HEIGHT, + Children = new Drawable[] + { + tabBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + channelTabs = new ChatTabControl + { + RelativeSizeAxes = Axes.Both, + }, + } }, - } + }, }, }; channelTabs.Current.ValueChanged += newChannel => CurrentChannel = newChannel; + channelTabs.ChannelSelectorActive.ValueChanged += value => channelSelection.State = value ? Visibility.Visible : Visibility.Hidden; + channelSelection.StateChanged += (overlay, state) => + { + channelTabs.ChannelSelectorActive.Value = state == Visibility.Visible; + + if (state == Visibility.Visible) + { + textbox.HoldFocus = false; + if (1f - chatHeight.Value < channel_selection_min_height) + { + chatContainer.ResizeHeightTo(1f - channel_selection_min_height, 800, Easing.OutQuint); + channelSelectionContainer.ResizeHeightTo(channel_selection_min_height, 800, Easing.OutQuint); + channelSelection.Show(); + chatHeight.Value = 1f - channel_selection_min_height; + } + } + else + { + textbox.HoldFocus = true; + } + }; } private double startDragChatHeight; + private bool isDragging; protected override bool OnDragStart(InputState state) { - if (!channelTabs.Hovering) + isDragging = tabsArea.IsHovered; + + if (!isDragging) return base.OnDragStart(state); startDragChatHeight = chatHeight.Value; @@ -148,10 +207,20 @@ namespace osu.Game.Overlays protected override bool OnDrag(InputState state) { - Trace.Assert(state.Mouse.PositionMouseDown != null); + if (isDragging) + { + Trace.Assert(state.Mouse.PositionMouseDown != null); - chatHeight.Value = startDragChatHeight - (state.Mouse.Position.Y - state.Mouse.PositionMouseDown.Value.Y) / Parent.DrawSize.Y; - return base.OnDrag(state); + chatHeight.Value = startDragChatHeight - (state.Mouse.Position.Y - state.Mouse.PositionMouseDown.Value.Y) / Parent.DrawSize.Y; + } + + return true; + } + + protected override bool OnDragEnd(InputState state) + { + isDragging = false; + return base.OnDragEnd(state); } public void APIStateChanged(APIAccess api, APIState state) @@ -173,26 +242,26 @@ namespace osu.Game.Overlays protected override void OnFocus(InputState state) { - //this is necessary as inputTextBox is masked away and therefore can't get focus :( - InputManager.ChangeFocus(inputTextBox); + //this is necessary as textbox is masked away and therefore can't get focus :( + GetContainingInputManager().ChangeFocus(textbox); base.OnFocus(state); } protected override void PopIn() { - MoveToY(0, transition_length, EasingTypes.OutQuint); - FadeIn(transition_length, EasingTypes.OutQuint); + this.MoveToY(0, transition_length, Easing.OutQuint); + this.FadeIn(transition_length, Easing.OutQuint); - inputTextBox.HoldFocus = true; + textbox.HoldFocus = true; base.PopIn(); } protected override void PopOut() { - MoveToY(Height, transition_length, EasingTypes.InSine); - FadeOut(transition_length, EasingTypes.InSine); + this.MoveToY(Height, transition_length, Easing.InSine); + this.FadeOut(transition_length, Easing.InSine); - inputTextBox.HoldFocus = false; + textbox.HoldFocus = false; base.PopOut(); } @@ -205,8 +274,9 @@ namespace osu.Game.Overlays chatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); chatHeight.ValueChanged += h => { - Height = (float)h; - tabBackground.FadeTo(Height == 1 ? 1 : 0.8f, 200); + chatContainer.Height = (float)h; + channelSelectionContainer.Height = 1f - (float)h; + tabBackground.FadeTo(h == 1 ? 1 : 0.8f, 200); }; chatHeight.TriggerChange(); @@ -221,14 +291,7 @@ namespace osu.Game.Overlays private void initializeChannels() { - SpriteText loading; - Add(loading = new OsuSpriteText - { - Text = @"initialising chat...", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - TextSize = 40, - }); + loading.Show(); messageRequest?.Cancel(); @@ -237,12 +300,19 @@ namespace osu.Game.Overlays { Scheduler.Add(delegate { - loading.FadeOut(100); - loading.Expire(); - addChannel(channels.Find(c => c.Name == @"#lazer")); addChannel(channels.Find(c => c.Name == @"#osu")); addChannel(channels.Find(c => c.Name == @"#lobby")); + + channelSelection.OnRequestJoin = addChannel; + channelSelection.Sections = new[] + { + new ChannelSection + { + Header = "All Channels", + Channels = channels, + }, + }; }); messageRequest = Scheduler.AddDelayed(fetchNewMessages, 1000, true); @@ -262,24 +332,36 @@ namespace osu.Game.Overlays set { - if (currentChannel == value) return; - - if (channelTabs.ChannelSelectorActive) return; - - if (currentChannel != null) - currentChannelContainer.Clear(false); + if (currentChannel == value || value == null) return; currentChannel = value; + textbox.Current.Disabled = currentChannel.ReadOnly; + channelTabs.Current.Value = value; + var loaded = loadedChannels.Find(d => d.Channel == value); if (loaded == null) - loadedChannels.Add(loaded = new DrawableChannel(currentChannel)); + { + currentChannelContainer.FadeOut(500, Easing.OutQuint); + loading.Show(); - inputTextBox.Current.Disabled = currentChannel.ReadOnly; + loaded = new DrawableChannel(currentChannel); + loadedChannels.Add(loaded); + LoadComponentAsync(loaded, l => + { + if (currentChannel.Messages.Any()) + loading.Hide(); - currentChannelContainer.Add(loaded); - - channelTabs.Current.Value = value; + currentChannelContainer.Clear(false); + currentChannelContainer.Add(loaded); + currentChannelContainer.FadeIn(500, Easing.OutQuint); + }); + } + else + { + currentChannelContainer.Clear(false); + currentChannelContainer.Add(loaded); + } } } @@ -296,7 +378,6 @@ namespace osu.Game.Overlays } else { - careChannels.Add(channel); channelTabs.AddItem(channel); } @@ -306,6 +387,8 @@ namespace osu.Game.Overlays if (CurrentChannel == null) CurrentChannel = channel; + + channel.Joined.Value = true; } private void fetchInitialMessages(Channel channel) @@ -314,6 +397,7 @@ namespace osu.Game.Overlays req.Success += delegate (List messages) { + loading.Hide(); channel.AddNewMessages(messages.ToArray()); Debug.Write("success!"); }; @@ -330,6 +414,7 @@ namespace osu.Game.Overlays if (fetchReq != null) return; fetchReq = new GetMessagesRequest(careChannels, lastMessageId); + fetchReq.Success += delegate (List messages) { foreach (var group in messages.Where(m => m.TargetType == TargetType.Channel).GroupBy(m => m.TargetId)) @@ -340,6 +425,7 @@ namespace osu.Game.Overlays Debug.Write("success!"); fetchReq = null; }; + fetchReq.Failure += delegate { Debug.Write("failure!"); @@ -353,51 +439,42 @@ namespace osu.Game.Overlays { var postText = textbox.Text; + textbox.Text = string.Empty; + if (string.IsNullOrEmpty(postText)) return; + var target = currentChannel; + + if (target == null) return; + if (!api.IsLoggedIn) { - currentChannel?.AddNewMessages(new ErrorMessage("Please login to participate in chat!")); - textbox.Text = string.Empty; + target.AddNewMessages(new ErrorMessage("Please login to participate in chat!")); return; } - if (currentChannel == null) return; - if (postText[0] == '/') { // TODO: handle commands - currentChannel.AddNewMessages(new ErrorMessage("Chat commands are not supported yet!")); - textbox.Text = string.Empty; + target.AddNewMessages(new ErrorMessage("Chat commands are not supported yet!")); return; } - var message = new Message + var message = new LocalEchoMessage { Sender = api.LocalUser.Value, Timestamp = DateTimeOffset.Now, - TargetType = TargetType.Channel, //TODO: read this from currentChannel - TargetId = currentChannel.Id, + TargetType = TargetType.Channel, //TODO: read this from channel + TargetId = target.Id, Content = postText }; - textbox.ReadOnly = true; var req = new PostMessageRequest(message); - req.Failure += e => - { - textbox.FlashColour(Color4.Red, 1000); - textbox.ReadOnly = false; - }; - - req.Success += m => - { - currentChannel.AddNewMessages(m); - - textbox.ReadOnly = false; - textbox.Text = string.Empty; - }; + target.AddLocalEcho(message); + req.Failure += e => target.ReplaceMessage(message, null); + req.Success += m => target.ReplaceMessage(message, m); api.Queue(req); } diff --git a/osu.Game/Overlays/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index 42819f7f87..9b19b8150e 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -14,10 +14,12 @@ using osu.Game.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; using OpenTK.Input; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Dialog { - public class PopupDialog : FocusedOverlayContainer + public class PopupDialog : OsuFocusedOverlayContainer { public static readonly float ENTER_DURATION = 500; public static readonly float EXIT_DURATION = 200; @@ -28,14 +30,14 @@ namespace osu.Game.Overlays.Dialog private readonly Container content; private readonly Container ring; private readonly FillFlowContainer buttonsContainer; - private readonly TextAwesome iconText; + private readonly SpriteIcon icon; private readonly SpriteText header; private readonly SpriteText body; public FontAwesome Icon { - get { return iconText.Icon; } - set { iconText.Icon = value; } + get { return icon.Icon; } + set { icon.Icon = value; } } public string HeaderText @@ -55,7 +57,7 @@ namespace osu.Game.Overlays.Dialog get { return buttonsContainer.Children; } set { - buttonsContainer.Children = value; + buttonsContainer.ChildrenEnumerable = value; foreach (PopupDialogButton b in value) { var action = b.Action; @@ -78,7 +80,7 @@ namespace osu.Game.Overlays.Dialog { if (args.Repeat) return false; - if (args.Key == Key.Enter) + if (args.Key == Key.Enter || args.Key == Key.KeypadEnter) { Buttons.OfType().FirstOrDefault()?.TriggerOnClick(); return true; @@ -113,17 +115,17 @@ namespace osu.Game.Overlays.Dialog ring.ResizeTo(ringMinifiedSize); } - content.FadeIn(ENTER_DURATION, EasingTypes.OutQuint); - ring.ResizeTo(ringSize, ENTER_DURATION, EasingTypes.OutQuint); - buttonsContainer.TransformSpacingTo(Vector2.Zero, ENTER_DURATION, EasingTypes.OutQuint); - buttonsContainer.MoveToY(0, ENTER_DURATION, EasingTypes.OutQuint); + content.FadeIn(ENTER_DURATION, Easing.OutQuint); + ring.ResizeTo(ringSize, ENTER_DURATION, Easing.OutQuint); + buttonsContainer.TransformSpacingTo(Vector2.Zero, ENTER_DURATION, Easing.OutQuint); + buttonsContainer.MoveToY(0, ENTER_DURATION, Easing.OutQuint); } protected override void PopOut() { base.PopOut(); - content.FadeOut(EXIT_DURATION, EasingTypes.InSine); + content.FadeOut(EXIT_DURATION, Easing.InSine); } public PopupDialog() @@ -145,7 +147,7 @@ namespace osu.Game.Overlays.Dialog { RelativeSizeAxes = Axes.Both, Masking = true, - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Colour = Color4.Black.Opacity(0.5f), @@ -203,12 +205,12 @@ namespace osu.Game.Overlays.Dialog RelativeSizeAxes = Axes.Both, Colour = Color4.Black.Opacity(0), }, - iconText = new TextAwesome + icon = new SpriteIcon { Origin = Anchor.Centre, Anchor = Anchor.Centre, Icon = FontAwesome.fa_close, - TextSize = 50, + Size = new Vector2(50), }, }, }, diff --git a/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs b/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs index 1449577b21..a17c4a7bf2 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogCancelButton.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Game.Graphics; namespace osu.Game.Overlays.Dialog @@ -10,11 +9,9 @@ namespace osu.Game.Overlays.Dialog public class PopupDialogCancelButton : PopupDialogButton { [BackgroundDependencyLoader] - private void load(OsuColour colours, AudioManager audio) + private void load(OsuColour colours) { ButtonColour = colours.Blue; - SampleHover = audio.Sample.Get(@"Menu/menuclick"); - SampleClick = audio.Sample.Get(@"Menu/menuback"); } } } diff --git a/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs b/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs index 46bf3debc4..3d99987080 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogOkButton.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Game.Graphics; namespace osu.Game.Overlays.Dialog @@ -10,11 +9,9 @@ namespace osu.Game.Overlays.Dialog public class PopupDialogOkButton : PopupDialogButton { [BackgroundDependencyLoader] - private void load(OsuColour colours, AudioManager audio) + private void load(OsuColour colours) { ButtonColour = colours.Pink; - SampleHover = audio.Sample.Get(@"Menu/menuclick"); - SampleClick = audio.Sample.Get(@"Menu/menu-play-click"); } } } diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 9454272728..012e93f10d 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -4,13 +4,14 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays { - public class DialogOverlay : FocusedOverlayContainer + public class DialogOverlay : OsuFocusedOverlayContainer { private readonly Container dialogContainer; private PopupDialog currentDialog; @@ -29,13 +30,12 @@ namespace osu.Game.Overlays State = Visibility.Visible; } - private void onDialogOnStateChanged(OverlayContainer dialog, Visibility v) + private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v) { if (v != Visibility.Hidden) return; //handle the dialog being dismissed. - dialog.Delay(PopupDialog.EXIT_DURATION); - dialog.Expire(); + dialog.Delay(PopupDialog.EXIT_DURATION).Expire(); if (dialog == currentDialog) State = Visibility.Hidden; @@ -44,13 +44,13 @@ namespace osu.Game.Overlays protected override void PopIn() { base.PopIn(); - FadeIn(PopupDialog.ENTER_DURATION, EasingTypes.OutQuint); + this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint); } protected override void PopOut() { base.PopOut(); - FadeOut(PopupDialog.EXIT_DURATION, EasingTypes.InSine); + this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine); } public DialogOverlay() diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/Direct/DirectGridPanel.cs index 4be68157d7..98ab5e88f8 100644 --- a/osu.Game/Overlays/Direct/DirectGridPanel.cs +++ b/osu.Game/Overlays/Direct/DirectGridPanel.cs @@ -1,19 +1,17 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Linq; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; namespace osu.Game.Overlays.Direct { @@ -30,7 +28,7 @@ namespace osu.Game.Overlays.Direct CornerRadius = 4; Masking = true; - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Offset = new Vector2(0f, 1f), @@ -43,14 +41,14 @@ namespace osu.Game.Overlays.Direct { base.LoadComplete(); - FadeInFromZero(200, EasingTypes.Out); + this.FadeInFromZero(200, Easing.Out); bottomPanel.LayoutDuration = 200; - bottomPanel.LayoutEasing = EasingTypes.Out; + bottomPanel.LayoutEasing = Easing.Out; bottomPanel.Origin = Anchor.BottomLeft; } [BackgroundDependencyLoader] - private void load(OsuColour colours, LocalisationEngine localisation, TextureStore textures) + private void load(OsuColour colours, LocalisationEngine localisation) { Children = new[] { @@ -59,7 +57,7 @@ namespace osu.Game.Overlays.Direct RelativeSizeAxes = Axes.Both, Colour = Color4.Black, }, - GetBackground(textures), + CreateBackground(), new Box { RelativeSizeAxes = Axes.Both, @@ -180,11 +178,11 @@ namespace osu.Game.Overlays.Direct Margin = new MarginPadding { Top = vertical_padding, Right = vertical_padding }, Children = new[] { - new Statistic(FontAwesome.fa_play_circle, SetInfo.Beatmaps.FirstOrDefault()?.OnlineInfo.PlayCount ?? 0) + new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0) { Margin = new MarginPadding { Right = 1 }, }, - new Statistic(FontAwesome.fa_heart, SetInfo.Beatmaps.FirstOrDefault()?.OnlineInfo.FavouriteCount ?? 0), + new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0), }, }, }; diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index 48636a5228..5b45fc7725 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -6,16 +6,15 @@ using OpenTK.Graphics; 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.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Database; using osu.Framework.Allocation; using osu.Framework.Localisation; -using osu.Framework.Graphics.Textures; -using System.Linq; using osu.Framework.Input; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Direct { @@ -31,7 +30,7 @@ namespace osu.Game.Overlays.Direct Height = height; CornerRadius = 5; Masking = true; - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Offset = new Vector2(0f, 1f), @@ -44,11 +43,11 @@ namespace osu.Game.Overlays.Direct { base.LoadComplete(); - FadeInFromZero(200, EasingTypes.Out); + this.FadeInFromZero(200, Easing.Out); } [BackgroundDependencyLoader] - private void load(LocalisationEngine localisation, TextureStore textures) + private void load(LocalisationEngine localisation) { Children = new[] { @@ -57,11 +56,11 @@ namespace osu.Game.Overlays.Direct RelativeSizeAxes = Axes.Both, Colour = Color4.Black, }, - GetBackground(textures), + CreateBackground(), new Box { RelativeSizeAxes = Axes.Both, - ColourInfo = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.25f), Color4.Black.Opacity(0.75f)), + Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.25f), Color4.Black.Opacity(0.75f)), }, new Container { @@ -104,11 +103,11 @@ namespace osu.Game.Overlays.Direct Margin = new MarginPadding { Right = height - vertical_padding * 2 + vertical_padding }, Children = new Drawable[] { - new Statistic(FontAwesome.fa_play_circle, SetInfo.Beatmaps.FirstOrDefault()?.OnlineInfo.PlayCount ?? 0) + new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0) { Margin = new MarginPadding { Right = 1 }, }, - new Statistic(FontAwesome.fa_heart, SetInfo.Beatmaps.FirstOrDefault()?.OnlineInfo.FavouriteCount ?? 0), + new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0), new FillFlowContainer { Anchor = Anchor.TopRight, @@ -151,20 +150,19 @@ namespace osu.Game.Overlays.Direct }; } - private class DownloadButton : ClickableContainer + private class DownloadButton : OsuClickableContainer { - private readonly TextAwesome icon; + private readonly SpriteIcon icon; public DownloadButton() { Children = new Drawable[] { - icon = new TextAwesome + icon = new SpriteIcon { Anchor = Anchor.Centre, Origin = Anchor.Centre, - UseFullGlyphHeight = false, - TextSize = 30, + Size = new Vector2(30), Icon = FontAwesome.fa_osu_chevron_down_o, }, }; @@ -172,25 +170,25 @@ namespace osu.Game.Overlays.Direct protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - icon.ScaleTo(0.9f, 1000, EasingTypes.Out); + icon.ScaleTo(0.9f, 1000, Easing.Out); return base.OnMouseDown(state, args); } protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) { - icon.ScaleTo(1f, 500, EasingTypes.OutElastic); + icon.ScaleTo(1f, 500, Easing.OutElastic); return base.OnMouseUp(state, args); } protected override bool OnHover(InputState state) { - icon.ScaleTo(1.1f, 500, EasingTypes.OutElastic); + icon.ScaleTo(1.1f, 500, Easing.OutElastic); return base.OnHover(state); } protected override void OnHoverLost(InputState state) { - icon.ScaleTo(1f, 500, EasingTypes.OutElastic); + icon.ScaleTo(1f, 500, Easing.OutElastic); } } } diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index 8a56cf392e..2f048b0e3d 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -2,14 +2,12 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using System.Linq; using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -24,7 +22,7 @@ namespace osu.Game.Overlays.Direct SetInfo = setInfo; } - protected IEnumerable GetDifficultyIcons() + protected List GetDifficultyIcons() { var icons = new List(); @@ -34,20 +32,25 @@ namespace osu.Game.Overlays.Direct return icons; } - protected Drawable GetBackground(TextureStore textures) + protected Drawable CreateBackground() => new DelayedLoadWrapper(new BeatmapSetCover(SetInfo) { - return new AsyncLoadWrapper(new BeatmapBackgroundSprite(new OnlineWorkingBeatmap(SetInfo.Beatmaps.FirstOrDefault(), textures, null)) - { - FillMode = FillMode.Fill, - OnLoadComplete = d => d.FadeInFromZero(400, EasingTypes.Out), - }) { RelativeSizeAxes = Axes.Both }; - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), + }) + { + RelativeSizeAxes = Axes.Both, + TimeBeforeLoad = 300 + }; public class Statistic : FillFlowContainer { private readonly SpriteText text; private int value; + public int Value { get { return value; } @@ -72,11 +75,11 @@ namespace osu.Game.Overlays.Direct { Font = @"Exo2.0-SemiBoldItalic", }, - new TextAwesome + new SpriteIcon { Icon = icon, Shadow = true, - TextSize = 14, + Size = new Vector2(14), Margin = new MarginPadding { Top = 1 }, }, }; diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs index 735e14b8c1..28d26d0641 100644 --- a/osu.Game/Overlays/Direct/FilterControl.cs +++ b/osu.Game/Overlays/Direct/FilterControl.cs @@ -5,142 +5,53 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Configuration; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Database; +using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.Containers; +using osu.Game.Overlays.SearchableList; +using osu.Game.Rulesets; namespace osu.Game.Overlays.Direct { - public class FilterControl : Container + public class FilterControl : SearchableListFilterControl { - public static readonly float HEIGHT = 35 + 32 + 30 + padding * 2; // search + mode toggle buttons + sort tabs + padding + public readonly Bindable Ruleset = new Bindable(); + private FillFlowContainer modeButtons; - private const float padding = 10; - - private readonly Box tabStrip; - private readonly FillFlowContainer modeButtons; - - public readonly SearchTextBox Search; - public readonly SortTabControl SortTabs; - public readonly OsuEnumDropdown RankStatusDropdown; - public readonly Bindable DisplayStyle = new Bindable(); - - protected override bool InternalContains(Vector2 screenSpacePos) => base.InternalContains(screenSpacePos) || RankStatusDropdown.Contains(screenSpacePos); - - public FilterControl() + protected override Color4 BackgroundColour => OsuColour.FromHex(@"384552"); + protected override DirectSortCriteria DefaultTab => DirectSortCriteria.Ranked; + protected override Drawable CreateSupplementaryControls() { - RelativeSizeAxes = Axes.X; - Height = HEIGHT; - DisplayStyle.Value = DirectOverlay.PanelDisplayStyle.Grid; - - Children = new Drawable[] + modeButtons = new FillFlowContainer { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"384552"), - Alpha = 0.9f, - }, - tabStrip = new Box - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.TopLeft, - RelativeSizeAxes = Axes.X, - Height = 1, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = DirectOverlay.WIDTH_PADDING, Right = DirectOverlay.WIDTH_PADDING }, - Children = new Drawable[] - { - Search = new DirectSearchTextBox - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = padding }, - }, - modeButtons = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(padding, 0f), - Margin = new MarginPadding { Top = padding }, - }, - SortTabs = new SortTabControl - { - RelativeSizeAxes = Axes.X, - }, - }, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Spacing = new Vector2(10f, 0f), - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Top = HEIGHT - SlimEnumDropdown.HEIGHT - padding, Right = DirectOverlay.WIDTH_PADDING }, - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5f, 0f), - Direction = FillDirection.Horizontal, - Children = new[] - { - new DisplayStyleToggleButton(FontAwesome.fa_th_large, DirectOverlay.PanelDisplayStyle.Grid, DisplayStyle), - new DisplayStyleToggleButton(FontAwesome.fa_list_ul, DirectOverlay.PanelDisplayStyle.List, DisplayStyle), - }, - }, - RankStatusDropdown = new SlimEnumDropdown - { - RelativeSizeAxes = Axes.None, - Width = 160f, - }, - }, - }, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10f, 0f), }; - RankStatusDropdown.Current.Value = RankStatus.RankedApproved; - SortTabs.Current.Value = SortCriteria.Title; - SortTabs.Current.TriggerChange(); + return modeButtons; } [BackgroundDependencyLoader(true)] - private void load(OsuGame game, RulesetDatabase rulesets, OsuColour colours) + private void load(OsuGame game, RulesetStore rulesets, OsuColour colours) { - tabStrip.Colour = colours.Yellow; - RankStatusDropdown.AccentColour = colours.BlueDark; + DisplayStyleControl.Dropdown.AccentColour = colours.BlueDark; - var b = new Bindable(); //backup bindable incase the game is null + Ruleset.BindTo(game?.Ruleset ?? new Bindable { Value = rulesets.GetRuleset(0) }); foreach (var r in rulesets.AllRulesets) { - modeButtons.Add(new RulesetToggleButton(game?.Ruleset ?? b, r)); + modeButtons.Add(new RulesetToggleButton(Ruleset, r)); } } - private class DirectSearchTextBox : SearchTextBox + private class RulesetToggleButton : OsuClickableContainer { - protected override Color4 BackgroundUnfocused => backgroundColour; - protected override Color4 BackgroundFocused => backgroundColour; - - private Color4 backgroundColour; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private Drawable icon { - backgroundColour = colours.Gray2.Opacity(0.9f); + get { return iconContainer.Icon; } + set { iconContainer.Icon = value; } } - } - - private class RulesetToggleButton : ClickableContainer - { - private readonly TextAwesome icon; private RulesetInfo ruleset; public RulesetInfo Ruleset @@ -149,15 +60,17 @@ namespace osu.Game.Overlays.Direct set { ruleset = value; - icon.Icon = Ruleset.CreateInstance().Icon; + icon = Ruleset.CreateInstance().CreateIcon(); } } private readonly Bindable bindable; + private readonly ConstrainedIconContainer iconContainer; + private void Bindable_ValueChanged(RulesetInfo obj) { - icon.FadeTo(Ruleset.ID == obj?.ID ? 1f : 0.5f, 100); + iconContainer.FadeTo(Ruleset.ID == obj?.ID ? 1f : 0.5f, 100); } public RulesetToggleButton(Bindable bindable, RulesetInfo ruleset) @@ -167,11 +80,11 @@ namespace osu.Game.Overlays.Direct Children = new[] { - icon = new TextAwesome + iconContainer = new ConstrainedIconContainer { Origin = Anchor.TopLeft, Anchor = Anchor.TopLeft, - TextSize = 32, + Size = new Vector2(32), } }; @@ -188,46 +101,16 @@ namespace osu.Game.Overlays.Direct base.Dispose(isDisposing); } } + } - private class DisplayStyleToggleButton : ClickableContainer - { - private readonly TextAwesome icon; - private readonly DirectOverlay.PanelDisplayStyle style; - private readonly Bindable bindable; - - public DisplayStyleToggleButton(FontAwesome icon, DirectOverlay.PanelDisplayStyle style, Bindable bindable) - { - this.bindable = bindable; - this.style = style; - Size = new Vector2(SlimEnumDropdown.HEIGHT); - - Children = new Drawable[] - { - this.icon = new TextAwesome - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = icon, - TextSize = 18, - UseFullGlyphHeight = false, - Alpha = 0.5f, - }, - }; - - bindable.ValueChanged += Bindable_ValueChanged; - Bindable_ValueChanged(bindable.Value); - Action = () => bindable.Value = this.style; - } - - private void Bindable_ValueChanged(DirectOverlay.PanelDisplayStyle style) - { - icon.FadeTo(style == this.style ? 1.0f : 0.5f, 100); - } - - protected override void Dispose(bool isDisposing) - { - bindable.ValueChanged -= Bindable_ValueChanged; - } - } + public enum DirectSortCriteria + { + Title, + Artist, + Creator, + Difficulty, + Ranked, + Rating, + Plays, } } diff --git a/osu.Game/Overlays/Direct/Header.cs b/osu.Game/Overlays/Direct/Header.cs index 8e4ede48d5..2c50fb453f 100644 --- a/osu.Game/Overlays/Direct/Header.cs +++ b/osu.Game/Overlays/Direct/Header.cs @@ -2,123 +2,38 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.ComponentModel; -using OpenTK; using OpenTK.Graphics; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; - -using Container = osu.Framework.Graphics.Containers.Container; +using osu.Game.Overlays.SearchableList; namespace osu.Game.Overlays.Direct { - public class Header : Container + public class Header : SearchableListHeader { - public static readonly float HEIGHT = 90; + protected override Color4 BackgroundColour => OsuColour.FromHex(@"252f3a"); + protected override float TabStripWidth => 298; - private readonly Box tabStrip; - - public readonly OsuTabControl Tabs; + protected override DirectTab DefaultTab => DirectTab.Search; + protected override Drawable CreateHeaderText() => new OsuSpriteText { Text = @"osu!direct", TextSize = 25 }; + protected override FontAwesome Icon => FontAwesome.fa_osu_chevron_down_o; public Header() { - Height = HEIGHT; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"252f3a"), - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = DirectOverlay.WIDTH_PADDING, Right = DirectOverlay.WIDTH_PADDING }, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomLeft, - Position = new Vector2(-35f, 5f), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f, 0f), - Children = new Drawable[] - { - new TextAwesome - { - TextSize = 25, - Icon = FontAwesome.fa_osu_chevron_down_o, - }, - new OsuSpriteText - { - TextSize = 25, - Text = @"osu!direct", - }, - }, - }, - tabStrip = new Box - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Width = 282, //todo: make this actually match the tab control's width instead of hardcoding - Height = 1, - }, - Tabs = new DirectTabControl - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - }, - }, - }, - }; - Tabs.Current.Value = DirectTab.Search; Tabs.Current.TriggerChange(); } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - tabStrip.Colour = colours.Green; - } - - private class DirectTabControl : OsuTabControl - { - protected override TabItem CreateTabItem(DirectTab value) => new DirectTabItem(value); - - public DirectTabControl() - { - Height = 25; - AccentColour = Color4.White; - } - - private class DirectTabItem : OsuTabItem - { - public DirectTabItem(DirectTab value) : base(value) - { - Text.TextSize = 15; - } - } - } } public enum DirectTab { Search, [Description("Newest Maps")] - NewestMaps, + NewestMaps = DirectSortCriteria.Ranked, [Description("Top Rated")] - TopRated, + TopRated = DirectSortCriteria.Rating, [Description("Most Played")] - MostPlayed + MostPlayed = DirectSortCriteria.Plays, } } diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index b7f6572bcc..b1c7dab778 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -5,30 +5,40 @@ using System.Collections.Generic; using System.Linq; using OpenTK; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using osu.Game.Database; +using osu.Framework.Threading; +using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Overlays.Direct; - -using Container = osu.Framework.Graphics.Containers.Container; +using osu.Game.Overlays.SearchableList; +using osu.Game.Rulesets; +using OpenTK.Graphics; namespace osu.Game.Overlays { - public class DirectOverlay : WaveOverlayContainer + public class DirectOverlay : SearchableListOverlay { - public static readonly int WIDTH_PADDING = 80; private const float panel_padding = 10f; - private readonly FilterControl filter; + private APIAccess api; + private RulesetStore rulesets; + private readonly FillFlowContainer resultCountsContainer; private readonly OsuSpriteText resultCountsText; private readonly FillFlowContainer panels; + protected override Color4 BackgroundColour => OsuColour.FromHex(@"485e74"); + protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"465b71"); + protected override Color4 TrianglesColourDark => OsuColour.FromHex(@"3f5265"); + + protected override SearchableListHeader CreateHeader() => new Header(); + protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); + private IEnumerable beatmapSets; public IEnumerable BeatmapSets { @@ -38,7 +48,18 @@ namespace osu.Game.Overlays if (beatmapSets?.Equals(value) ?? false) return; beatmapSets = value; - recreatePanels(filter.DisplayStyle.Value); + if (BeatmapSets == null) + { + foreach (var p in panels.Children) + { + p.FadeOut(200); + p.Expire(); + } + + return; + } + + recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); } } @@ -66,109 +87,90 @@ namespace osu.Game.Overlays ThirdWaveColour = OsuColour.FromHex(@"005774"); FourthWaveColour = OsuColour.FromHex(@"003a4e"); - Header header; - Children = new Drawable[] + ScrollFlow.Children = new Drawable[] { - new Box + resultCountsContainer = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"485e74"), - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new[] + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Top = 5 }, + Children = new Drawable[] { - new Triangles + new OsuSpriteText { - RelativeSizeAxes = Axes.Both, - TriangleScale = 5, - ColourLight = OsuColour.FromHex(@"465b71"), - ColourDark = OsuColour.FromHex(@"3f5265"), + Text = "Found ", + TextSize = 15, }, - }, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = Header.HEIGHT + FilterControl.HEIGHT }, - Children = new[] - { - new ScrollContainer + resultCountsText = new OsuSpriteText { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Children = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - resultCountsContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = WIDTH_PADDING, Top = 6 }, - Children = new Drawable[] - { - new OsuSpriteText - { - Text = "Found ", - TextSize = 15, - }, - resultCountsText = new OsuSpriteText - { - TextSize = 15, - Font = @"Exo2.0-Bold", - }, - } - }, - panels = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Top = panel_padding, Bottom = panel_padding, Left = WIDTH_PADDING, Right = WIDTH_PADDING }, - Spacing = new Vector2(panel_padding), - }, - }, - }, - }, + TextSize = 15, + Font = @"Exo2.0-Bold", }, - }, + } }, - filter = new FilterControl - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = Header.HEIGHT }, - }, - header = new Header + panels = new FillFlowContainer { RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(panel_padding), + Margin = new MarginPadding { Top = 10 }, }, }; - header.Tabs.Current.ValueChanged += tab => { if (tab != DirectTab.Search) filter.Search.Current.Value = string.Empty; }; + Filter.Search.Current.ValueChanged += text => { if (text != string.Empty) Header.Tabs.Current.Value = DirectTab.Search; }; + ((FilterControl)Filter).Ruleset.ValueChanged += ruleset => Scheduler.AddOnce(updateSearch); + Filter.DisplayStyleControl.DisplayStyle.ValueChanged += recreatePanels; + Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += rankStatus => Scheduler.AddOnce(updateSearch); - filter.Search.Exit = Hide; - filter.Search.Current.ValueChanged += text => { if (text != string.Empty) header.Tabs.Current.Value = DirectTab.Search; }; - filter.DisplayStyle.ValueChanged += recreatePanels; + Header.Tabs.Current.ValueChanged += tab => + { + if (tab != DirectTab.Search) + { + currentQuery.Value = string.Empty; + Filter.Tabs.Current.Value = (DirectSortCriteria)Header.Tabs.Current.Value; + Scheduler.AddOnce(updateSearch); + } + }; + + currentQuery.ValueChanged += v => + { + queryChangedDebounce?.Cancel(); + + if (string.IsNullOrEmpty(v)) + Scheduler.AddOnce(updateSearch); + else + { + BeatmapSets = null; + ResultAmounts = null; + + queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500); + } + }; + + currentQuery.BindTo(Filter.Search.Current); + + Filter.Tabs.Current.ValueChanged += sortCriteria => + { + if (Header.Tabs.Current.Value != DirectTab.Search && sortCriteria != (DirectSortCriteria)Header.Tabs.Current.Value) + Header.Tabs.Current.Value = DirectTab.Search; + + Scheduler.AddOnce(updateSearch); + }; updateResultCounts(); } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, APIAccess api, RulesetStore rulesets) { + this.api = api; + this.rulesets = rulesets; resultCountsContainer.Colour = colours.Yellow; } private void updateResultCounts() { - resultCountsContainer.FadeTo(ResultAmounts == null ? 0f : 1f, 200, EasingTypes.Out); + resultCountsContainer.FadeTo(ResultAmounts == null ? 0f : 1f, 200, Easing.OutQuint); if (ResultAmounts == null) return; resultCountsText.Text = pluralize("Artist", ResultAmounts.Artists) + ", " + @@ -184,32 +186,67 @@ namespace osu.Game.Overlays private void recreatePanels(PanelDisplayStyle displayStyle) { if (BeatmapSets == null) return; - panels.Children = BeatmapSets.Select(b => displayStyle == PanelDisplayStyle.Grid ? (DirectPanel)new DirectGridPanel(b) { Width = 400 } : new DirectListPanel(b)); + + panels.ChildrenEnumerable = BeatmapSets.Select(b => + { + switch (displayStyle) + { + case PanelDisplayStyle.Grid: + return new DirectGridPanel(b) { Width = 400 }; + default: + return new DirectListPanel(b); + } + }); } - public override bool AcceptsFocus => true; + private GetBeatmapSetsRequest getSetsRequest; - protected override bool OnClick(InputState state) => true; + private readonly Bindable currentQuery = new Bindable(); - protected override void OnFocus(InputState state) + private ScheduledDelegate queryChangedDebounce; + + private void updateSearch() { - InputManager.ChangeFocus(filter.Search); - base.OnFocus(state); + queryChangedDebounce?.Cancel(); + + if (!IsLoaded) return; + + BeatmapSets = null; + ResultAmounts = null; + + getSetsRequest?.Cancel(); + + if (api == null) return; + + if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty)) return; + + getSetsRequest = new GetBeatmapSetsRequest(currentQuery, + ((FilterControl)Filter).Ruleset.Value, + Filter.DisplayStyleControl.Dropdown.Current.Value, + Filter.Tabs.Current.Value); //todo: sort direction (?) + + getSetsRequest.Success += r => + { + BeatmapSets = r?.Select(response => response.ToBeatmapSet(rulesets)); + if (BeatmapSets == null) return; + + var artists = new List(); + var songs = new List(); + var tags = new List(); + foreach (var s in BeatmapSets) + { + artists.Add(s.Metadata.Artist); + songs.Add(s.Metadata.Title); + tags.AddRange(s.Metadata.Tags.Split(' ')); + } + + ResultAmounts = new ResultCounts(distinctCount(artists), distinctCount(songs), distinctCount(tags)); + }; + + api.Queue(getSetsRequest); } - protected override void PopIn() - { - base.PopIn(); - - filter.Search.HoldFocus = true; - } - - protected override void PopOut() - { - base.PopOut(); - - filter.Search.HoldFocus = false; - } + private int distinctCount(List list) => list.Distinct().ToArray().Length; public class ResultCounts { @@ -224,11 +261,5 @@ namespace osu.Game.Overlays Tags = tags; } } - - public enum PanelDisplayStyle - { - Grid, - List, - } } } diff --git a/osu.Game/Overlays/DragBar.cs b/osu.Game/Overlays/DragBar.cs deleted file mode 100644 index bb28f08553..0000000000 --- a/osu.Game/Overlays/DragBar.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Transforms; -using osu.Framework.Input; -using OpenTK; - -namespace osu.Game.Overlays -{ - public class DragBar : Container - { - protected readonly Container Fill; - - public Action SeekRequested; - - public bool IsSeeking { get; private set; } - - private bool enabled = true; - public bool IsEnabled - { - get { return enabled; } - set - { - enabled = value; - if (!enabled) - Fill.Width = 0; - } - } - - public DragBar() - { - RelativeSizeAxes = Axes.X; - - Children = new Drawable[] - { - Fill = new Container - { - Name = "FillContainer", - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Width = 0, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both - } - } - } - }; - } - - public void UpdatePosition(float position) - { - if (IsSeeking || !IsEnabled) return; - - updatePosition(position, false); - } - - private void seek(InputState state) - { - float seekLocation = state.Mouse.Position.X / DrawWidth; - - if (!IsEnabled) return; - - SeekRequested?.Invoke(seekLocation); - updatePosition(seekLocation); - } - - private void updatePosition(float position, bool easing = true) - { - position = MathHelper.Clamp(position, 0, 1); - Fill.TransformTo(() => Fill.Width, position, easing ? 200 : 0, EasingTypes.OutQuint, new TransformSeek()); - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - seek(state); - return true; - } - - protected override bool OnDrag(InputState state) - { - seek(state); - return true; - } - - protected override bool OnDragStart(InputState state) => IsSeeking = true; - - protected override bool OnDragEnd(InputState state) - { - IsSeeking = false; - return true; - } - - private class TransformSeek : TransformFloat - { - public override void Apply(Drawable d) - { - base.Apply(d); - d.Width = CurrentValue; - } - } - } -} diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs new file mode 100644 index 0000000000..7dd9919e5d --- /dev/null +++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Input.Bindings; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.KeyBinding +{ + public class GlobalKeyBindingsSection : KeyBindingsSection + { + private readonly string name; + + public override FontAwesome Icon => FontAwesome.fa_osu_mod_nofail; + public override string Header => name; + + public GlobalKeyBindingsSection(KeyBindingInputManager manager, string name) + { + this.name = name; + + Defaults = manager.DefaultKeyBindings; + } + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs new file mode 100644 index 0000000000..6c697c8660 --- /dev/null +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -0,0 +1,362 @@ +// 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.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Input; +using OpenTK.Graphics; +using OpenTK.Input; + +namespace osu.Game.Overlays.KeyBinding +{ + internal class KeyBindingRow : Container, IFilterable + { + private readonly Enum action; + private readonly IEnumerable bindings; + + private const float transition_time = 150; + + private const float height = 20; + + private const float padding = 5; + + private bool matchingFilter; + + public bool MatchingFilter + { + get { return matchingFilter; } + set + { + matchingFilter = value; + this.FadeTo(!matchingFilter ? 0 : 1); + } + } + + private OsuSpriteText text; + private OsuSpriteText pressAKey; + + private FillFlowContainer buttons; + + public string[] FilterTerms => new[] { text.Text }.Concat(bindings.Select(b => b.KeyCombination.ReadableString())).ToArray(); + + public KeyBindingRow(Enum action, IEnumerable bindings) + { + this.action = action; + this.bindings = bindings; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Masking = true; + CornerRadius = padding; + } + + private KeyBindingStore store; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, KeyBindingStore store) + { + this.store = store; + + EdgeEffect = new EdgeEffectParameters + { + Radius = 2, + Colour = colours.YellowDark.Opacity(0), + Type = EdgeEffectType.Shadow, + Hollow = true, + }; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.6f, + }, + text = new OsuSpriteText + { + Text = action.GetDescription(), + Margin = new MarginPadding(padding), + }, + buttons = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight + }, + pressAKey = new OsuSpriteText + { + Text = "Press a key to change binding, DEL to delete, ESC to cancel.", + Y = height, + Margin = new MarginPadding(padding), + Alpha = 0, + Colour = colours.YellowDark + } + }; + + foreach (var b in bindings) + buttons.Add(new KeyButton(b)); + } + + protected override bool OnHover(InputState state) + { + this.FadeEdgeEffectTo(1, transition_time, Easing.OutQuint); + + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + this.FadeEdgeEffectTo(0, transition_time, Easing.OutQuint); + + base.OnHoverLost(state); + } + + public override bool AcceptsFocus => bindTarget == null; + + private KeyButton bindTarget; + + public bool AllowMainMouseButtons; + + private bool isModifier(Key k) => k < Key.F1; + + protected override bool OnClick(InputState state) => true; + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (!HasFocus || !bindTarget.IsHovered) + return base.OnMouseDown(state, args); + + if (!AllowMainMouseButtons) + { + switch (args.Button) + { + case MouseButton.Left: + case MouseButton.Right: + return true; + } + } + + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); + return true; + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + // don't do anything until the last button is released. + if (!HasFocus || state.Mouse.Buttons.Any()) + return base.OnMouseUp(state, args); + + if (bindTarget.IsHovered) + finalise(); + else + updateBindTarget(); + return true; + } + + protected override bool OnWheel(InputState state) + { + if (HasFocus) + { + if (bindTarget.IsHovered) + { + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); + finalise(); + return true; + } + } + + return base.OnWheel(state); + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (!HasFocus) + return false; + + switch (args.Key) + { + case Key.Escape: + finalise(); + return true; + case Key.Delete: + bindTarget.UpdateKeyCombination(InputKey.None); + finalise(); + return true; + } + + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); + if (!isModifier(args.Key)) finalise(); + + return true; + } + + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + { + if (!HasFocus) return base.OnKeyUp(state, args); + + finalise(); + return true; + } + + private void finalise() + { + if (bindTarget != null) + { + store.Update(bindTarget.KeyBinding); + + bindTarget.IsBinding = false; + Schedule(() => + { + // schedule to ensure we don't instantly get focus back on next OnMouseClick (see AcceptFocus impl.) + bindTarget = null; + }); + } + + if (HasFocus) + GetContainingInputManager().ChangeFocus(null); + + pressAKey.FadeOut(300, Easing.OutQuint); + pressAKey.Padding = new MarginPadding { Bottom = -pressAKey.DrawHeight }; + } + + protected override void OnFocus(InputState state) + { + AutoSizeDuration = 500; + AutoSizeEasing = Easing.OutQuint; + + pressAKey.FadeIn(300, Easing.OutQuint); + pressAKey.Padding = new MarginPadding(); + + updateBindTarget(); + base.OnFocus(state); + } + + protected override void OnFocusLost(InputState state) + { + finalise(); + base.OnFocusLost(state); + } + + private void updateBindTarget() + { + if (bindTarget != null) bindTarget.IsBinding = false; + bindTarget = buttons.FirstOrDefault(b => b.IsHovered) ?? buttons.FirstOrDefault(); + if (bindTarget != null) bindTarget.IsBinding = true; + } + + private class KeyButton : Container + { + public readonly Framework.Input.Bindings.KeyBinding KeyBinding; + + private readonly Box box; + public readonly OsuSpriteText Text; + + private Color4 hoverColour; + + private bool isBinding; + + public bool IsBinding + { + get { return isBinding; } + set + { + if (value == isBinding) return; + isBinding = value; + + updateHoverState(); + } + } + + public KeyButton(Framework.Input.Bindings.KeyBinding keyBinding) + { + KeyBinding = keyBinding; + + Margin = new MarginPadding(padding); + + // todo: use this in a meaningful way + // var isDefault = keyBinding.Action is Enum; + + Masking = true; + CornerRadius = padding; + + Height = height; + AutoSizeAxes = Axes.X; + + Children = new Drawable[] + { + new Container + { + AlwaysPresent = true, + Width = 80, + Height = height, + }, + box = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + Text = new OsuSpriteText + { + Font = "Venera", + TextSize = 10, + Margin = new MarginPadding(5), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = keyBinding.KeyCombination.ReadableString(), + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + hoverColour = colours.YellowDark; + } + + protected override bool OnHover(InputState state) + { + updateHoverState(); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + updateHoverState(); + base.OnHoverLost(state); + } + + private void updateHoverState() + { + if (isBinding) + { + box.FadeColour(Color4.White, transition_time, Easing.OutQuint); + Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint); + } + else + { + box.FadeColour(IsHovered ? hoverColour : Color4.Black, transition_time, Easing.OutQuint); + Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint); + } + } + + public void UpdateKeyCombination(KeyCombination newCombination) + { + KeyBinding.KeyCombination = newCombination; + Text.Text = KeyBinding.KeyCombination.ReadableString(); + } + } + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSection.cs new file mode 100644 index 0000000000..44c28ee9c8 --- /dev/null +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSection.cs @@ -0,0 +1,49 @@ +// 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.Allocation; +using osu.Game.Input; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets; +using OpenTK; + +namespace osu.Game.Overlays.KeyBinding +{ + public abstract class KeyBindingsSection : SettingsSection + { + protected IEnumerable Defaults; + + protected RulesetInfo Ruleset; + + protected KeyBindingsSection() + { + FlowContent.Spacing = new Vector2(0, 1); + } + + [BackgroundDependencyLoader] + private void load(KeyBindingStore store) + { + var enumType = Defaults?.FirstOrDefault()?.Action?.GetType(); + + if (enumType == null) return; + + // for now let's just assume a variant of zero. + // this will need to be implemented in a better way in the future. + int? variant = null; + if (Ruleset != null) + variant = 0; + + var bindings = store.Query(Ruleset?.ID, variant); + + foreach (Enum v in Enum.GetValues(enumType)) + // one row per valid action. + Add(new KeyBindingRow(v, bindings.Where(b => b.Action.Equals((int)(object)v))) + { + AllowMainMouseButtons = Ruleset != null + }); + } + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs new file mode 100644 index 0000000000..20941115e3 --- /dev/null +++ b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.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 + +using osu.Game.Graphics; +using osu.Game.Rulesets; + +namespace osu.Game.Overlays.KeyBinding +{ + public class RulesetBindingsSection : KeyBindingsSection + { + public override FontAwesome Icon => FontAwesome.fa_osu_mod_nofail; + public override string Header => Ruleset.Name; + + public RulesetBindingsSection(RulesetInfo ruleset) + { + Ruleset = ruleset; + + Defaults = ruleset.CreateInstance().GetDefaultKeyBindings(); + } + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/KeyBindingOverlay.cs b/osu.Game/Overlays/KeyBindingOverlay.cs new file mode 100644 index 0000000000..827d361099 --- /dev/null +++ b/osu.Game/Overlays/KeyBindingOverlay.cs @@ -0,0 +1,31 @@ +// 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; +using osu.Game.Input.Bindings; +using osu.Game.Overlays.KeyBinding; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets; + +namespace osu.Game.Overlays +{ + public class KeyBindingOverlay : SettingsOverlay + { + protected override Drawable CreateHeader() => new SettingsHeader("key configuration", "Customise your keys!"); + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(RulesetStore rulesets, GlobalKeyBindingInputManager global) + { + AddSection(new GlobalKeyBindingsSection(global, "Global")); + + foreach (var ruleset in rulesets.AllRulesets) + AddSection(new RulesetBindingsSection(ruleset)); + } + + public KeyBindingOverlay() + : base(false) + { + } + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index c3f41270ce..1bce31c789 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -4,14 +4,15 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Overlays.Settings.Sections.General; using OpenTK.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays { - internal class LoginOverlay : FocusedOverlayContainer + internal class LoginOverlay : OsuFocusedOverlayContainer { private LoginSettings settingsSection; @@ -27,7 +28,8 @@ namespace osu.Game.Overlays { Children = new Drawable[] { - new Box { + new Box + { RelativeSizeAxes = Axes.Both, Colour = Color4.Black, Alpha = 0.6f, @@ -38,12 +40,13 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y, Masking = true, AutoSizeDuration = transition_time, - AutoSizeEasing = EasingTypes.OutQuint, + AutoSizeEasing = Easing.OutQuint, Children = new Drawable[] { settingsSection = new LoginSettings { Padding = new MarginPadding(10), + RequestHide = Hide, }, new Box { @@ -64,9 +67,9 @@ namespace osu.Game.Overlays base.PopIn(); settingsSection.Bounding = true; - FadeIn(transition_time, EasingTypes.OutQuint); + this.FadeIn(transition_time, Easing.OutQuint); - InputManager.ChangeFocus(settingsSection); + GetContainingInputManager().ChangeFocus(settingsSection); } protected override void PopOut() @@ -74,7 +77,7 @@ namespace osu.Game.Overlays base.PopOut(); settingsSection.Bounding = false; - FadeOut(transition_time); + this.FadeOut(transition_time); } } } diff --git a/osu.Game/Overlays/MainSettings.cs b/osu.Game/Overlays/MainSettings.cs new file mode 100644 index 0000000000..b4d9cac045 --- /dev/null +++ b/osu.Game/Overlays/MainSettings.cs @@ -0,0 +1,151 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.Settings; +using osu.Game.Overlays.Settings.Sections; +using osu.Game.Screens.Ranking; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays +{ + public class MainSettings : SettingsOverlay + { + private readonly KeyBindingOverlay keyBindingOverlay; + private BackButton backButton; + + protected override IEnumerable CreateSections() => new SettingsSection[] + { + new GeneralSection(), + new GraphicsSection(), + new GameplaySection(), + new AudioSection(), + new SkinSection(), + new InputSection(keyBindingOverlay), + new OnlineSection(), + new MaintenanceSection(), + new DebugSection(), + }; + + protected override Drawable CreateHeader() => new SettingsHeader("settings", "Change the way osu! behaves"); + protected override Drawable CreateFooter() => new SettingsFooter(); + + public MainSettings() + : base(true) + { + keyBindingOverlay = new KeyBindingOverlay + { + Depth = 1, + Anchor = Anchor.TopRight, + }; + keyBindingOverlay.StateChanged += keyBindingOverlay_StateChanged; + } + + public override bool AcceptsFocus => keyBindingOverlay.State != Visibility.Visible; + + private const float hidden_width = 120; + + private void keyBindingOverlay_StateChanged(VisibilityContainer container, Visibility visibility) + { + switch (visibility) + { + case Visibility.Visible: + Background.FadeTo(0.9f, 300, Easing.OutQuint); + Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint); + + SectionsContainer.FadeOut(300, Easing.OutQuint); + ContentContainer.MoveToX(hidden_width - WIDTH, 500, Easing.OutQuint); + + backButton.Delay(100).FadeIn(100); + break; + case Visibility.Hidden: + Background.FadeTo(0.6f, 500, Easing.OutQuint); + Sidebar?.FadeColour(Color4.White, 300, Easing.OutQuint); + + SectionsContainer.FadeIn(500, Easing.OutQuint); + ContentContainer.MoveToX(0, 500, Easing.OutQuint); + + backButton.FadeOut(100); + break; + } + } + + protected override float ExpandedPosition => keyBindingOverlay.State == Visibility.Visible ? hidden_width - WIDTH : base.ExpandedPosition; + + [BackgroundDependencyLoader] + private void load() + { + ContentContainer.Add(keyBindingOverlay); + + ContentContainer.Add(backButton = new BackButton + { + Alpha = 0, + Width = hidden_width, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Action = () => keyBindingOverlay.Hide() + }); + } + + private class BackButton : OsuClickableContainer + { + private AspectContainer aspect; + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + aspect = new AspectContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = -15, + Size = new Vector2(15), + Shadow = true, + Icon = FontAwesome.fa_chevron_left + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = 15, + TextSize = 12, + Font = @"Exo2.0-Bold", + Text = @"back", + }, + } + } + }; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + aspect.ScaleTo(0.75f, 2000, Easing.OutQuint); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + aspect.ScaleTo(1, 1000, Easing.OutElastic); + return base.OnMouseUp(state, args); + } + } + } +} diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs new file mode 100644 index 0000000000..880b607e78 --- /dev/null +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -0,0 +1,309 @@ +// 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 osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Sprites; +using osu.Game.Users; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Overlays.MedalSplash; +using osu.Framework.Allocation; +using osu.Framework.Audio.Sample; +using osu.Framework.Audio; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input; +using OpenTK.Input; +using System.Linq; +using osu.Framework.Graphics.Shapes; +using System; +using osu.Framework.MathUtils; + +namespace osu.Game.Overlays +{ + public class MedalOverlay : FocusedOverlayContainer + { + public const float DISC_SIZE = 400; + + private const float border_width = 5; + + private readonly Medal medal; + private readonly Box background; + private readonly Container backgroundStrip, particleContainer; + private readonly BackgroundStrip leftStrip, rightStrip; + private readonly CircularContainer disc; + private readonly Sprite innerSpin, outerSpin; + private DrawableMedal drawableMedal; + + private SampleChannel getSample; + + public MedalOverlay(Medal medal) + { + this.medal = medal; + RelativeSizeAxes = Axes.Both; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(60), + }, + outerSpin = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(DISC_SIZE + 500), + Alpha = 0f, + }, + backgroundStrip = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = border_width, + Alpha = 0f, + Children = new[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Width = 0.5f, + Padding = new MarginPadding { Right = DISC_SIZE / 2 }, + Children = new[] + { + leftStrip = new BackgroundStrip(0f, 1f) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Width = 0.5f, + Padding = new MarginPadding { Left = DISC_SIZE / 2 }, + Children = new[] + { + rightStrip = new BackgroundStrip(1f, 0f), + }, + }, + }, + }, + particleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0f, + }, + disc = new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0f, + Masking = true, + AlwaysPresent = true, + BorderColour = Color4.White, + BorderThickness = border_width, + Size = new Vector2(DISC_SIZE), + Scale = new Vector2(0.8f), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"05262f"), + }, + new Triangles + { + RelativeSizeAxes = Axes.Both, + TriangleScale = 2, + ColourDark = OsuColour.FromHex(@"04222b"), + ColourLight = OsuColour.FromHex(@"052933"), + }, + innerSpin = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(1.05f), + Alpha = 0.25f, + }, + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, TextureStore textures, AudioManager audio) + { + getSample = audio.Sample.Get(@"MedalSplash/medal-get"); + innerSpin.Texture = outerSpin.Texture = textures.Get(@"MedalSplash/disc-spin"); + + disc.EdgeEffect = leftStrip.EdgeEffect = rightStrip.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colours.Blue.Opacity(0.5f), + Radius = 50, + }; + + disc.Add(drawableMedal = new DrawableMedal(medal) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Show(); + } + + protected override void Update() + { + base.Update(); + + particleContainer.Add(new MedalParticle(RNG.Next(0, 359))); + } + + protected override bool OnClick(InputState state) + { + dismiss(); + return true; + } + + protected override void OnFocusLost(InputState state) + { + if (state.Keyboard.Keys.Contains(Key.Escape)) dismiss(); + } + + private const double initial_duration = 400; + private const double step_duration = 900; + + protected override void PopIn() + { + base.PopIn(); + + this.FadeIn(200); + background.FlashColour(Color4.White.Opacity(0.25f), 400); + + getSample.Play(); + + innerSpin.Spin(20000, RotationDirection.Clockwise); + outerSpin.Spin(40000, RotationDirection.Clockwise); + + using (BeginDelayedSequence(200, true)) + { + disc.FadeIn(initial_duration) + .ScaleTo(1f, initial_duration * 2, Easing.OutElastic); + + particleContainer.FadeIn(initial_duration); + outerSpin.FadeTo(0.1f, initial_duration * 2); + + using (BeginDelayedSequence(initial_duration + 200, true)) + { + backgroundStrip.FadeIn(step_duration); + leftStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint); + rightStrip.ResizeWidthTo(1f, step_duration, Easing.OutQuint); + + this.Animate().Schedule(() => + { + if (drawableMedal.State != DisplayState.Full) + drawableMedal.State = DisplayState.Icon; + }) + .Delay(step_duration).Schedule(() => + { + if (drawableMedal.State != DisplayState.Full) + drawableMedal.State = DisplayState.MedalUnlocked; + }) + .Delay(step_duration).Schedule(() => + { + if (drawableMedal.State != DisplayState.Full) + drawableMedal.State = DisplayState.Full; + }); + } + } + } + + protected override void PopOut() + { + base.PopOut(); + this.FadeOut(200); + } + + private void dismiss() + { + if (drawableMedal.State != DisplayState.Full) + { + // if we haven't yet, play out the animation fully + drawableMedal.State = DisplayState.Full; + FinishTransforms(true); + return; + } + + Hide(); + Expire(); + } + + private class BackgroundStrip : Container + { + public BackgroundStrip(float start, float end) + { + RelativeSizeAxes = Axes.Both; + Width = 0f; + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(start), Color4.White.Opacity(end)); + Masking = true; + + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + } + }; + } + } + + private class MedalParticle : CircularContainer + { + private readonly float direction; + + private Vector2 positionForOffset(float offset) => new Vector2((float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction))); + + public MedalParticle(float direction) + { + this.direction = direction; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Position = positionForOffset(DISC_SIZE / 2); + Masking = true; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = colours.Blue.Opacity(0.5f), + Radius = 5, + }; + + this.MoveTo(positionForOffset(DISC_SIZE / 2 + 200), 500); + this.FadeOut(500); + Expire(); + } + } + } +} diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs new file mode 100644 index 0000000000..56b26e7176 --- /dev/null +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -0,0 +1,186 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework; +using OpenTK; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Users; + +namespace osu.Game.Overlays.MedalSplash +{ + public class DrawableMedal : Container, IStateful + { + private const float scale_when_unlocked = 0.76f; + private const float scale_when_full = 0.6f; + + private readonly Medal medal; + private readonly Container medalContainer; + private readonly Sprite medalSprite, medalGlow; + private readonly OsuSpriteText unlocked, name; + private readonly TextFlowContainer description; + private readonly FillFlowContainer infoFlow; + private DisplayState state; + public DrawableMedal(Medal medal) + { + this.medal = medal; + Position = new Vector2(0f, MedalOverlay.DISC_SIZE / 2); + + Children = new Drawable[] + { + medalContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Alpha = 0f, + Children = new Drawable[] + { + medalSprite = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.81f), + }, + medalGlow = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }, + }, + unlocked = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Medal Unlocked".ToUpper(), + TextSize = 24, + Font = @"Exo2.0-Light", + Alpha = 0f, + Scale = new Vector2(1f / scale_when_unlocked), + }, + infoFlow = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.6f, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0f, 5f), + Children = new Drawable[] + { + name = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = medal.Name, + TextSize = 20, + Font = @"Exo2.0-Bold", + Alpha = 0f, + Scale = new Vector2(1f / scale_when_full), + }, + description = new TextFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0f, + Scale = new Vector2(1f / scale_when_full), + }, + }, + }, + }; + + description.AddText(medal.Description, s => + { + s.Anchor = Anchor.TopCentre; + s.Origin = Anchor.TopCentre; + s.TextSize = 16; + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, TextureStore textures) + { + medalSprite.Texture = textures.Get(medal.ImageUrl); + medalGlow.Texture = textures.Get(@"MedalSplash/medal-glow"); + description.Colour = colours.BlueLight; + + unlocked.Position = new Vector2(0f, medalContainer.Size.Y / 2 + 10); + infoFlow.Position = new Vector2(0f, unlocked.Position.Y + 90); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateState(); + } + + public DisplayState State + { + get { return state; } + set + { + if (state == value) return; + + state = value; + updateState(); + } + } + + private void updateState() + { + if (!IsLoaded) return; + + const double duration = 900; + + switch (state) + { + case DisplayState.None: + medalContainer.ScaleTo(0); + break; + case DisplayState.Icon: + medalContainer + .FadeIn(duration) + .ScaleTo(1, duration, Easing.OutElastic); + break; + case DisplayState.MedalUnlocked: + medalContainer + .FadeTo(1) + .ScaleTo(1); + + this.ScaleTo(scale_when_unlocked, duration, Easing.OutExpo); + this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 30, duration, Easing.OutExpo); + unlocked.FadeInFromZero(duration); + break; + case DisplayState.Full: + medalContainer + .FadeTo(1) + .ScaleTo(1); + + this.ScaleTo(scale_when_full, duration, Easing.OutExpo); + this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 60, duration, Easing.OutExpo); + name.FadeInFromZero(duration + 100); + description.FadeInFromZero(duration * 2); + break; + } + + + } + } + + public enum DisplayState + { + None, + Icon, + MedalUnlocked, + Full, + } +} diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index dd135e43ef..3ca4a204a5 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -20,7 +20,6 @@ using osu.Framework.Graphics.Cursor; namespace osu.Game.Overlays.Mods { - /// /// Represents a clickable button which can cycle through one of more mods. /// @@ -36,7 +35,7 @@ namespace osu.Game.Overlays.Mods public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty; - private const EasingTypes mod_switch_easing = EasingTypes.InOutSine; + private const Easing mod_switch_easing = Easing.InOutSine; private const double mod_switch_duration = 120; // A selected index of -1 means not selected. @@ -68,8 +67,8 @@ namespace osu.Game.Overlays.Mods if (beforeSelected != Selected) { - iconsContainer.RotateTo(Selected ? 5f : 0f, 300, EasingTypes.OutElastic); - iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, EasingTypes.OutElastic); + iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic); + iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic); } if (modBefore != modAfter) @@ -80,15 +79,17 @@ namespace osu.Game.Overlays.Mods backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); backgroundIcon.Icon = modAfter.Icon; - using (iconsContainer.BeginDelayedSequence(mod_switch_duration, true)) + using (BeginDelayedSequence(mod_switch_duration, true)) { - foregroundIcon.RotateTo(-rotate_angle * direction); - foregroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing); + foregroundIcon + .RotateTo(-rotate_angle * direction) + .RotateTo(0f, mod_switch_duration, mod_switch_easing); - backgroundIcon.RotateTo(rotate_angle * direction); - backgroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing); + backgroundIcon + .RotateTo(rotate_angle * direction) + .RotateTo(0f, mod_switch_duration, mod_switch_easing); - iconsContainer.Schedule(() => displayMod(modAfter)); + Schedule(() => displayMod(modAfter)); } } @@ -152,8 +153,8 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleOn = audio.Sample.Get(@"Checkbox/check-on"); - sampleOff = audio.Sample.Get(@"Checkbox/check-off"); + sampleOn = audio.Sample.Get(@"UI/check-on"); + sampleOff = audio.Sample.Get(@"UI/check-off"); } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) @@ -172,7 +173,7 @@ namespace osu.Game.Overlays.Mods public void SelectNext() { - (++SelectedIndex == -1 ? sampleOff : sampleOn).Play(); + (++SelectedIndex == Mods.Length ? sampleOff : sampleOn).Play(); Action?.Invoke(SelectedMod); } @@ -200,7 +201,7 @@ namespace osu.Game.Overlays.Mods iconsContainer.Clear(); if (Mods.Length > 1) { - iconsContainer.Add(new[] + iconsContainer.AddRange(new[] { backgroundIcon = new ModIcon(Mods[1]) { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index c001d613e4..eb643f390f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -8,7 +8,6 @@ using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; @@ -16,7 +15,8 @@ using osu.Game.Rulesets.Mods; using System; using System.Collections.Generic; using System.Linq; -using osu.Game.Database; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets; namespace osu.Game.Overlays.Mods { @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Mods } [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours, OsuGame osu, RulesetDatabase rulesets) + private void load(OsuColour colours, OsuGame osu, RulesetStore rulesets) { lowMultiplierColour = colours.Red; highMultiplierColour = colours.Green; @@ -66,14 +66,14 @@ namespace osu.Game.Overlays.Mods { base.PopOut(); - rankedMultiplerContainer.MoveToX(rankedMultiplerContainer.DrawSize.X, APPEAR_DURATION, EasingTypes.InSine); - rankedMultiplerContainer.FadeOut(APPEAR_DURATION, EasingTypes.InSine); + rankedMultiplerContainer.MoveToX(rankedMultiplerContainer.DrawSize.X, APPEAR_DURATION, Easing.InSine); + rankedMultiplerContainer.FadeOut(APPEAR_DURATION, Easing.InSine); foreach (ModSection section in modSectionsContainer.Children) { - section.ButtonsContainer.TransformSpacingTo(new Vector2(100f, 0f), APPEAR_DURATION, EasingTypes.InSine); - section.ButtonsContainer.MoveToX(100f, APPEAR_DURATION, EasingTypes.InSine); - section.ButtonsContainer.FadeOut(APPEAR_DURATION, EasingTypes.InSine); + section.ButtonsContainer.TransformSpacingTo(new Vector2(100f, 0f), APPEAR_DURATION, Easing.InSine); + section.ButtonsContainer.MoveToX(100f, APPEAR_DURATION, Easing.InSine); + section.ButtonsContainer.FadeOut(APPEAR_DURATION, Easing.InSine); } } @@ -81,14 +81,14 @@ namespace osu.Game.Overlays.Mods { base.PopIn(); - rankedMultiplerContainer.MoveToX(0, ranked_multiplier_duration, EasingTypes.OutQuint); - rankedMultiplerContainer.FadeIn(ranked_multiplier_duration, EasingTypes.OutQuint); + rankedMultiplerContainer.MoveToX(0, ranked_multiplier_duration, Easing.OutQuint); + rankedMultiplerContainer.FadeIn(ranked_multiplier_duration, Easing.OutQuint); foreach (ModSection section in modSectionsContainer.Children) { - section.ButtonsContainer.TransformSpacingTo(new Vector2(50f, 0f), button_duration, EasingTypes.OutQuint); - section.ButtonsContainer.MoveToX(0, button_duration, EasingTypes.OutQuint); - section.ButtonsContainer.FadeIn(button_duration, EasingTypes.OutQuint); + section.ButtonsContainer.TransformSpacingTo(new Vector2(50f, 0f), button_duration, Easing.OutQuint); + section.ButtonsContainer.MoveToX(0, button_duration, Easing.OutQuint); + section.ButtonsContainer.FadeIn(button_duration, Easing.OutQuint); } } diff --git a/osu.Game/Overlays/Music/CollectionsDropdown.cs b/osu.Game/Overlays/Music/CollectionsDropdown.cs new file mode 100644 index 0000000000..0c0a636be8 --- /dev/null +++ b/osu.Game/Overlays/Music/CollectionsDropdown.cs @@ -0,0 +1,73 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Music +{ + public class CollectionsDropdown : OsuDropdown + { + protected override DropdownHeader CreateHeader() => new CollectionsHeader { AccentColour = AccentColour }; + protected override Menu CreateMenu() => new CollectionsMenu(); + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.Gray6; + } + + private class CollectionsHeader : OsuDropdownHeader + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Gray4; + } + + public CollectionsHeader() + { + CornerRadius = 5; + Height = 30; + Icon.Size = new Vector2(14); + Icon.Margin = new MarginPadding(0); + Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 10, Right = 10 }; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.3f), + Radius = 3, + Offset = new Vector2(0f, 1f), + }; + } + } + + private class CollectionsMenu : OsuMenu + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Background.Colour = colours.Gray4; + } + + public CollectionsMenu() + { + CornerRadius = 5; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.3f), + Radius = 3, + Offset = new Vector2(0f, 1f), + }; + } + } + } +} diff --git a/osu.Game/Overlays/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs index 6d8790a9e8..56cd6e864b 100644 --- a/osu.Game/Overlays/Music/FilterControl.cs +++ b/osu.Game/Overlays/Music/FilterControl.cs @@ -3,10 +3,8 @@ using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using OpenTK; @@ -74,63 +72,5 @@ namespace osu.Game.Overlays.Music backgroundColour = colours.Gray2; } } - - private class CollectionsDropdown : OsuDropdown - { - protected override DropdownHeader CreateHeader() => new CollectionsHeader { AccentColour = AccentColour }; - protected override Menu CreateMenu() => new CollectionsMenu(); - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - AccentColour = colours.Gray6; - } - - private class CollectionsHeader : OsuDropdownHeader - { - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BackgroundColour = colours.Gray4; - } - - public CollectionsHeader() - { - CornerRadius = 5; - Height = 30; - Icon.TextSize = 14; - Icon.Margin = new MarginPadding(0); - Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 10, Right = 10 }; - EdgeEffect = new EdgeEffect - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.3f), - Radius = 3, - Offset = new Vector2(0f, 1f), - }; - } - } - - private class CollectionsMenu : OsuMenu - { - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Background.Colour = colours.Gray4; - } - - public CollectionsMenu() - { - CornerRadius = 5; - EdgeEffect = new EdgeEffect - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.3f), - Radius = 3, - Offset = new Vector2(0f, 1f), - }; - } - } - } } } diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 0618f96cac..4145a8d1f0 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -2,15 +2,17 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; +using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Database; -using osu.Game.Graphics; -using OpenTK.Graphics; -using osu.Framework.Localisation; using osu.Framework.Graphics.Sprites; -using System.Collections.Generic; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using OpenTK; namespace osu.Game.Overlays.Music { @@ -21,8 +23,8 @@ namespace osu.Game.Overlays.Music private Color4 hoverColour; private Color4 artistColour; - private TextAwesome handle; - private Paragraph text; + private SpriteIcon handle; + private TextFlowContainer text; private IEnumerable titleSprites; private UnicodeBindableString titleBind; private UnicodeBindableString artistBind; @@ -40,7 +42,7 @@ namespace osu.Game.Overlays.Music if (value == selected) return; selected = value; - Flush(true); + FinishTransforms(true); foreach (SpriteText s in titleSprites) s.FadeColour(Selected ? hoverColour : Color4.White, fade_duration); } @@ -66,18 +68,17 @@ namespace osu.Game.Overlays.Music Children = new Drawable[] { - handle = new TextAwesome + handle = new SpriteIcon { Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - TextSize = 12, + Size = new Vector2(12), Colour = colours.Gray5, Icon = FontAwesome.fa_bars, Alpha = 0f, - Margin = new MarginPadding { Left = 5 }, - Padding = new MarginPadding { Top = 2 }, + Margin = new MarginPadding { Left = 5, Top = 2 }, }, - text = new Paragraph + text = new OsuTextFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -137,17 +138,14 @@ namespace osu.Game.Overlays.Music public bool MatchingFilter { + get { return matching; } set { if (matching == value) return; matching = value; - FadeTo(matching ? 1 : 0, 200); - } - get - { - return matching; + this.FadeTo(matching ? 1 : 0, 200); } } } diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index eeb072fb00..3dd514edeb 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -6,7 +6,8 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Database; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Music { @@ -49,7 +50,7 @@ namespace osu.Game.Overlays.Music { Children = new Drawable[] { - new ScrollContainer + new OsuScrollContainer { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -72,6 +73,17 @@ namespace osu.Game.Overlays.Music }; } + public void AddBeatmapSet(BeatmapSetInfo beatmapSet) + { + items.Add(new PlaylistItem(beatmapSet) { OnSelect = itemSelected }); + } + + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) + { + PlaylistItem itemToRemove = items.Children.FirstOrDefault(item => item.BeatmapSetInfo.ID == beatmapSet.ID); + if (itemToRemove != null) items.Remove(itemToRemove); + } + private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren { public string[] FilterTerms => new string[] { }; @@ -89,7 +101,7 @@ namespace osu.Game.Overlays.Music public ItemSearchContainer() { LayoutDuration = 200; - LayoutEasing = EasingTypes.OutQuint; + LayoutEasing = Easing.OutQuint; } } } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 82596252b3..83e92c5554 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -3,21 +3,16 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Graphics; using OpenTK; using OpenTK.Graphics; -using osu.Framework.Extensions; -using osu.Framework.Input; namespace osu.Game.Overlays.Music { @@ -30,20 +25,16 @@ namespace osu.Game.Overlays.Music private FilterControl filter; private PlaylistList list; - private TrackManager trackManager; - private BeatmapDatabase beatmaps; + private BeatmapManager beatmaps; private readonly Bindable beatmapBacking = new Bindable(); public IEnumerable BeatmapSets; - private InputManager inputManager; [BackgroundDependencyLoader] - private void load(OsuGameBase game, BeatmapDatabase beatmaps, OsuColour colours, UserInputManager inputManager) + private void load(OsuGameBase game, BeatmapManager beatmaps, OsuColour colours) { - this.inputManager = inputManager; this.beatmaps = beatmaps; - trackManager = game.Audio.Track; Children = new Drawable[] { @@ -52,7 +43,7 @@ namespace osu.Game.Overlays.Music RelativeSizeAxes = Axes.Both, CornerRadius = 5, Masking = true, - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Colour = Color4.Black.Opacity(40), @@ -83,11 +74,16 @@ namespace osu.Game.Overlays.Music }, }; - list.BeatmapSets = BeatmapSets = beatmaps.GetAllWithChildren().ToList(); + beatmaps.BeatmapSetAdded += s => Schedule(() => list.AddBeatmapSet(s)); + beatmaps.BeatmapSetRemoved += s => Schedule(() => list.RemoveBeatmapSet(s)); + + list.BeatmapSets = BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); + beatmapBacking.BindTo(game.Beatmap); - filter.Search.OnCommit = (sender, newText) => { + filter.Search.OnCommit = (sender, newText) => + { var beatmap = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); if (beatmap != null) playSpecified(beatmap); }; @@ -103,18 +99,18 @@ namespace osu.Game.Overlays.Music protected override void PopIn() { filter.Search.HoldFocus = true; - Schedule(() => inputManager.ChangeFocus(filter.Search)); + Schedule(() => GetContainingInputManager().ChangeFocus(filter.Search)); - ResizeTo(new Vector2(1, playlist_height), transition_duration, EasingTypes.OutQuint); - FadeIn(transition_duration, EasingTypes.OutQuint); + this.ResizeTo(new Vector2(1, playlist_height), transition_duration, Easing.OutQuint); + this.FadeIn(transition_duration, Easing.OutQuint); } protected override void PopOut() { filter.Search.HoldFocus = false; - ResizeTo(new Vector2(1, 0), transition_duration, EasingTypes.OutQuint); - FadeOut(transition_duration); + this.ResizeTo(new Vector2(1, 0), transition_duration, Easing.OutQuint); + this.FadeOut(transition_duration); } private void itemSelected(BeatmapSetInfo set) @@ -153,13 +149,7 @@ namespace osu.Game.Overlays.Music private void playSpecified(BeatmapInfo info) { beatmapBacking.Value = beatmaps.GetWorkingBeatmap(info, beatmapBacking); - - Task.Run(() => - { - var track = beatmapBacking.Value.Track; - trackManager.SetExclusive(track); - track.Start(); - }).ContinueWith(task => Schedule(task.ThrowIfFaulted), TaskContinuationOptions.OnlyOnFaulted); + beatmapBacking.Value.Track.Start(); } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index f1be55801f..d970089942 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -12,20 +12,23 @@ using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Localisation; +using osu.Framework.Threading; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Framework.Threading; using osu.Game.Overlays.Music; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays { - public class MusicController : FocusedOverlayContainer + public class MusicController : OsuFocusedOverlayContainer { private const float player_height = 130; @@ -36,10 +39,12 @@ namespace osu.Game.Overlays private const float bottom_black_area_height = 55; private Drawable currentBackground; - private DragBar progressBar; + private ProgressBar progressBar; - private Button playButton; - private Button playlistButton; + private IconButton prevButton; + private IconButton playButton; + private IconButton nextButton; + private IconButton playlistButton; private SpriteText title, artist; @@ -56,6 +61,9 @@ namespace osu.Game.Overlays { Width = 400; Margin = new MarginPadding(10); + + // required to let MusicController handle beatmap cycling. + AlwaysPresent = true; } protected override bool OnDragStart(InputState state) => true; @@ -75,7 +83,7 @@ namespace osu.Game.Overlays protected override bool OnDragEnd(InputState state) { - dragContainer.MoveTo(Vector2.Zero, 800, EasingTypes.OutElastic); + dragContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic); return base.OnDragEnd(state); } @@ -105,7 +113,7 @@ namespace osu.Game.Overlays Height = player_height, Masking = true, CornerRadius = 5, - EdgeEffect = new EdgeEffect + EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Colour = Color4.Black.Opacity(40), @@ -143,7 +151,7 @@ namespace osu.Game.Overlays Anchor = Anchor.BottomCentre, Children = new Drawable[] { - new FillFlowContainer