diff --git a/.vscode/launch.json b/.vscode/launch.json index b8c026d891..506915f462 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,13 +1,13 @@ { "version": "0.2.0", "configurations": [{ - "name": "osu! (VisualTests)", + "name": "osu! VisualTests (Debug)", "windows": { "type": "clr" }, "type": "mono", "request": "launch", - "program": "${workspaceRoot}/osu.Game/bin/Debug/osu!.exe", + "program": "${workspaceRoot}/osu.Desktop/bin/Debug/osu!.exe", "args": [ "--tests" ], @@ -18,13 +18,30 @@ "console": "internalConsole" }, { - "name": "osu! (debug)", + "name": "osu! VisualTests (Release)", "windows": { "type": "clr" }, "type": "mono", "request": "launch", - "program": "${workspaceRoot}/osu.Game/bin/Debug/osu!.exe", + "program": "${workspaceRoot}/osu.Desktop/bin/Release/osu!.exe", + "args": [ + "--tests" + ], + "cwd": "${workspaceRoot}", + "preLaunchTask": "Build (Release)", + "runtimeExecutable": null, + "env": {}, + "console": "internalConsole" + }, + { + "name": "osu! (Debug)", + "windows": { + "type": "clr" + }, + "type": "mono", + "request": "launch", + "program": "${workspaceRoot}/osu.Desktop/bin/Debug/osu!.exe", "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug)", "runtimeExecutable": null, @@ -32,13 +49,13 @@ "console": "internalConsole" }, { - "name": "osu! (release)", + "name": "osu! (Release)", "windows": { "type": "clr" }, "type": "mono", "request": "launch", - "program": "${workspaceRoot}/osu.Game/bin/Release/osu!.exe", + "program": "${workspaceRoot}/osu.Desktop/bin/Release/osu!.exe", "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release)", "runtimeExecutable": null, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3db43ca9bb..f67d7a8c4e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,62 +2,70 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", - "command": "msbuild", - "type": "shell", - "suppressTaskName": true, - "args": [ - "/property:GenerateFullPaths=true", - "/property:DebugType=portable", - "/verbosity:minimal", - "/m" //parallel compiling support. - ], "tasks": [{ - "taskName": "Build (Debug)", + "label": "Build (Debug)", + "type": "shell", + "command": "msbuild", + "args": [ + "/p:GenerateFullPaths=true", + "/p:DebugType=portable", + "/m", + "/v:m" + ], "group": { "kind": "build", "isDefault": true }, - "problemMatcher": [ - "$msCompile" - ] + "problemMatcher": "$msCompile" }, { - "taskName": "Build (Release)", + "label": "Build (Release)", + "type": "shell", + "command": "msbuild", "args": [ - "/property:Configuration=Release" + "/p:Configuration=Release", + "/p:DebugType=portable", + "/p:GenerateFullPaths=true", + "/m", + "/v:m" ], - "problemMatcher": [ - "$msCompile" - ] + "group": "build", + "problemMatcher": "$msCompile" }, { - "taskName": "Clean (Debug)", + "label": "Clean (Debug)", + "type": "shell", + "command": "msbuild", "args": [ - "/target:Clean" + "/p:DebugType=portable", + "/p:GenerateFullPaths=true", + "/m", + "/t:Clean", + "/v:m" ], - "problemMatcher": [ - "$msCompile" - ] + "problemMatcher": "$msCompile" }, { - "taskName": "Clean (Release)", + "label": "Clean (Release)", + "type": "shell", + "command": "msbuild", "args": [ - "/target:Clean", - "/property:Configuration=Release" + "/p:Configuration=Release", + "/p:GenerateFullPaths=true", + "/p:DebugType=portable", + "/m", + "/t:Clean", + "/v:m" ], - "problemMatcher": [ - "$msCompile" - ] + "problemMatcher": "$msCompile" }, { - "taskName": "Clean All", + "label": "Clean All", "dependsOn": [ "Clean (Debug)", "Clean (Release)" ], - "problemMatcher": [ - "$msCompile" - ] + "problemMatcher": "$msCompile" } ] } \ No newline at end of file diff --git a/README.md b/README.md index ad56178132..856536d22d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ This is still heavily under development and is not intended for end-user use. Th We welcome all contributions, but keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention on having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time, to ensure no effort is wasted. -Contributions can be made via pull requests to this repository. We hope to credit and reward larger contributions via a [bounty system](https://www.bountysource.com/teams/ppy). If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu-framework/issues). +Please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**. + +Contributions can be made via pull requests to this repository. We hope to credit and reward larger contributions via a [bounty system](https://www.bountysource.com/teams/ppy). If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues). Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured; with any libraries we are using; with any processes involved with contributing, *please* bring it up. I welcome all feedback so we can make contributing to this project as pain-free as possible. diff --git a/appveyor.yml b/appveyor.yml index 21c15724e6..9cf68803a2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,7 @@ # 2017-09-14 clone_depth: 1 version: '{branch}-{build}' +image: Visual Studio 2017 configuration: Debug cache: - C:\ProgramData\chocolatey\bin -> appveyor.yml @@ -8,17 +9,17 @@ cache: - inspectcode -> appveyor.yml - packages -> **\packages.config install: - - cmd: git submodule update --init --recursive + - cmd: git submodule update --init --recursive --depth=5 - cmd: choco install resharper-clt -y - cmd: choco install nvika -y - - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.2/CodeFileSanity.exe + - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.3/CodeFileSanity.exe before_build: - cmd: CodeFileSanity.exe - - cmd: nuget restore + - cmd: nuget restore -verbosity quiet build: project: osu.sln parallel: true verbosity: minimal after_build: - - cmd: inspectcode /o="inspectcodereport.xml" /caches-home="inspectcode" osu.sln + - cmd: inspectcode --o="inspectcodereport.xml" --projects:osu.Game* --caches-home="inspectcode" osu.sln > NUL - cmd: NVika parsereport "inspectcodereport.xml" --treatwarningsaserrors \ No newline at end of file diff --git a/osu-framework b/osu-framework index cdb031c3a8..08f85f9bf9 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit cdb031c3a8ef693cd71458c5e19c68127ab72938 +Subproject commit 08f85f9bf9a7376aec8dfcde8c7c96d267d8c295 diff --git a/osu-resources b/osu-resources index a4418111f8..4287ee8043 160000 --- a/osu-resources +++ b/osu-resources @@ -1 +1 @@ -Subproject commit a4418111f8ed2350a6fd46fe69258884f0757745 +Subproject commit 4287ee8043fb1419017359bc3a5db5dc06bc643f diff --git a/osu.Desktop.Deploy/App.config b/osu.Desktop.Deploy/App.config index 2fae7a5e1c..2fbea810f6 100644 --- a/osu.Desktop.Deploy/App.config +++ b/osu.Desktop.Deploy/App.config @@ -13,7 +13,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste - + diff --git a/osu.Desktop.Deploy/Program.cs b/osu.Desktop.Deploy/Program.cs index 785f915a3e..e90fb1e567 100644 --- a/osu.Desktop.Deploy/Program.cs +++ b/osu.Desktop.Deploy/Program.cs @@ -7,7 +7,6 @@ using System.Configuration; using System.Diagnostics; using System.IO; using System.Linq; -using System.Net; using Newtonsoft.Json; using osu.Framework.IO.Network; using FileWebRequest = osu.Framework.IO.Network.FileWebRequest; @@ -19,7 +18,7 @@ namespace osu.Desktop.Deploy { private const string nuget_path = @"packages\NuGet.CommandLine.4.3.0\tools\NuGet.exe"; private const string squirrel_path = @"packages\squirrel.windows.1.7.8\tools\Squirrel.exe"; - private const string msbuild_path = @"C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe"; + private const string msbuild_path = @"C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe"; public static string StagingFolder = ConfigurationManager.AppSettings["StagingFolder"]; public static string ReleasesFolder = ConfigurationManager.AppSettings["ReleasesFolder"]; @@ -146,6 +145,8 @@ namespace osu.Desktop.Deploy /// private static void checkReleaseFiles() { + if (!canGitHub) return; + var releaseLines = getReleaseLines(); //ensure we have all files necessary @@ -158,6 +159,8 @@ namespace osu.Desktop.Deploy private static void pruneReleases() { + if (!canGitHub) return; + write("Pruning RELEASES..."); var releaseLines = getReleaseLines().ToList(); @@ -191,7 +194,7 @@ namespace osu.Desktop.Deploy private static void uploadBuild(string version) { - if (string.IsNullOrEmpty(GitHubAccessToken) || string.IsNullOrEmpty(codeSigningCertPath)) + if (!canGitHub || string.IsNullOrEmpty(CodeSigningCertificate)) return; write("Publishing to GitHub..."); @@ -229,8 +232,12 @@ namespace osu.Desktop.Deploy private static void openGitHubReleasePage() => Process.Start(GitHubReleasePage); + private static bool canGitHub => !string.IsNullOrEmpty(GitHubAccessToken); + private static void checkGitHubReleases() { + if (!canGitHub) return; + write("Checking GitHub releases..."); var req = new JsonWebRequest>($"{GitHubApiEndpoint}"); req.AuthenticatedBlockingPerform(); @@ -392,7 +399,7 @@ namespace osu.Desktop.Deploy public static void AuthenticatedBlockingPerform(this WebRequest r) { r.AddHeader("Authorization", $"token {GitHubAccessToken}"); - r.BlockingPerform(); + r.Perform(); } } @@ -402,12 +409,7 @@ namespace osu.Desktop.Deploy { } - protected override HttpWebRequest CreateWebRequest(string requestString = null) - { - var req = base.CreateWebRequest(requestString); - req.Accept = "application/octet-stream"; - return req; - } + protected override string Accept => "application/octet-stream"; } internal class ReleaseLine diff --git a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj b/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj index 6727a86a91..3bec56d322 100644 --- a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj +++ b/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -22,7 +22,6 @@ DEBUG;TRACE prompt 4 - 6 AnyCPU @@ -102,9 +101,6 @@ - - osu.licenseheader - PreserveNewest diff --git a/osu.Desktop/OpenTK.dll.config b/osu.Desktop/OpenTK.dll.config new file mode 100644 index 0000000000..5620e3d9e2 --- /dev/null +++ b/osu.Desktop/OpenTK.dll.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/osu.Game/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs similarity index 67% rename from osu.Game/OsuGameDesktop.cs rename to osu.Desktop/OsuGameDesktop.cs index 47e64a0d5b..3393bbf7fb 100644 --- a/osu.Game/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -7,22 +7,23 @@ using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; -using System.Windows.Forms; using Microsoft.Win32; +using osu.Desktop.Overlays; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; -using osu.Game.Overlays; -using osu.Game.Screens.Menu; +using osu.Game; +using OpenTK.Input; -namespace osu.Game +namespace osu.Desktop { internal class OsuGameDesktop : OsuGame { - private VersionManager versionManager; + private readonly bool noVersionOverlay; public OsuGameDesktop(string[] args = null) : base(args) { + noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false; } public override Storage GetStorageForStableInstall() @@ -81,16 +82,14 @@ namespace osu.Game { base.LoadComplete(); - LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }); - - ScreenChanged += s => + if (!noVersionOverlay) { - if (s is Intro && s.ChildScreen == null) + LoadComponentAsync(new VersionManager { Depth = int.MinValue }, v => { - Add(versionManager); - versionManager.State = Visibility.Visible; - } - }; + Add(v); + v.State = Visibility.Visible; + }); + } } public override void SetHost(GameHost host) @@ -104,19 +103,16 @@ namespace osu.Game desktopWindow.Icon = new Icon(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico")); desktopWindow.Title = Name; - desktopWindow.DragEnter += dragEnter; - desktopWindow.DragDrop += dragDrop; + desktopWindow.FileDrop += fileDrop; } } - private void dragDrop(DragEventArgs e) + private void fileDrop(object sender, FileDropEventArgs e) { - // this method will only be executed if e.Effect in dragEnter gets set to something other that None. - var dropData = (object[])e.Data.GetData(DataFormats.FileDrop); - var filePaths = dropData.Select(f => f.ToString()).ToArray(); + var filePaths = new [] { e.FileName }; if (filePaths.All(f => Path.GetExtension(f) == @".osz")) - Task.Run(() => BeatmapManager.Import(filePaths)); + Task.Factory.StartNew(() => BeatmapManager.Import(filePaths), TaskCreationOptions.LongRunning); else if (filePaths.All(f => Path.GetExtension(f) == @".osr")) Task.Run(() => { @@ -126,16 +122,5 @@ namespace osu.Game } private static readonly string[] allowed_extensions = { @".osz", @".osr" }; - - private void dragEnter(DragEventArgs e) - { - // dragDrop will only be executed if e.Effect gets set to something other that None in this method. - bool isFile = e.Data.GetDataPresent(DataFormats.FileDrop); - if (isFile) - { - var paths = ((object[])e.Data.GetData(DataFormats.FileDrop)).Select(f => f.ToString()).ToArray(); - e.Effect = allowed_extensions.Any(ext => paths.All(p => p.EndsWith(ext))) ? DragDropEffects.Copy : DragDropEffects.None; - } - } } } diff --git a/osu.Game/OsuTestBrowser.cs b/osu.Desktop/OsuTestBrowser.cs similarity index 92% rename from osu.Game/OsuTestBrowser.cs rename to osu.Desktop/OsuTestBrowser.cs index b0864e441f..23617de1c0 100644 --- a/osu.Game/OsuTestBrowser.cs +++ b/osu.Desktop/OsuTestBrowser.cs @@ -3,9 +3,10 @@ using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game; using osu.Game.Screens.Backgrounds; -namespace osu.Game +namespace osu.Desktop { internal class OsuTestBrowser : OsuGameBase { diff --git a/osu.Game/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs similarity index 93% rename from osu.Game/Overlays/VersionManager.cs rename to osu.Desktop/Overlays/VersionManager.cs index 7b0b3520cb..9e13003c3f 100644 --- a/osu.Game/Overlays/VersionManager.cs +++ b/osu.Desktop/Overlays/VersionManager.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Net.Http; using osu.Framework.Allocation; using osu.Framework.Development; using osu.Framework.Graphics; @@ -13,15 +12,17 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; +using osu.Game; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using OpenTK; using OpenTK.Graphics; using Squirrel; -namespace osu.Game.Overlays +namespace osu.Desktop.Overlays { public class VersionManager : OverlayContainer { @@ -196,10 +197,9 @@ namespace osu.Game.Overlays } } } - catch (HttpRequestException) + catch (Exception) { - //likely have no internet connection. - //we'll ignore this and retry later. + // we'll ignore this and retry later. can be triggered by no internet connection or thread abortion. } finally { @@ -231,7 +231,8 @@ namespace osu.Game.Overlays Text = @"Update ready to install. Click to restart!", Activated = () => { - UpdateManager.RestartAppWhenExited(); + // Squirrel returns execution to us after the update process is started, so it's safe to use Wait() here + UpdateManager.RestartAppWhenExited().Wait(); game.GracefullyExit(); return true; } diff --git a/osu.Game/Program.cs b/osu.Desktop/Program.cs similarity index 95% rename from osu.Game/Program.cs rename to osu.Desktop/Program.cs index 8044e9fa87..720b38144c 100644 --- a/osu.Game/Program.cs +++ b/osu.Desktop/Program.cs @@ -8,7 +8,7 @@ using osu.Framework; using osu.Framework.Platform; using osu.Game.IPC; -namespace osu.Game +namespace osu.Desktop { public static class Program { diff --git a/osu.Desktop/Properties/AssemblyInfo.cs b/osu.Desktop/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..2ed304ebd7 --- /dev/null +++ b/osu.Desktop/Properties/AssemblyInfo.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("osu!lazer")] +[assembly: AssemblyDescription("click the circles. to the beat.")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("ppy Pty Ltd")] +[assembly: AssemblyProduct("osu!lazer")] +[assembly: AssemblyCopyright("ppy Pty Ltd 2007-2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b0cb1d48-e4c2-4612-a347-beea7b1a71e7")] + +[assembly: AssemblyVersion("0.0.0")] +[assembly: AssemblyFileVersion("0.0.0")] diff --git a/osu.Game/Properties/app.manifest b/osu.Desktop/Properties/app.manifest similarity index 100% rename from osu.Game/Properties/app.manifest rename to osu.Desktop/Properties/app.manifest diff --git a/osu.Desktop/app.config b/osu.Desktop/app.config new file mode 100644 index 0000000000..ea1576b3d8 --- /dev/null +++ b/osu.Desktop/app.config @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/osu.Game/lazer.ico b/osu.Desktop/lazer.ico similarity index 100% rename from osu.Game/lazer.ico rename to osu.Desktop/lazer.ico diff --git a/osu.Desktop/osu!.res b/osu.Desktop/osu!.res new file mode 100644 index 0000000000..7c70e30401 Binary files /dev/null and b/osu.Desktop/osu!.res differ diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj new file mode 100644 index 0000000000..e4e9807754 --- /dev/null +++ b/osu.Desktop/osu.Desktop.csproj @@ -0,0 +1,284 @@ + + + + + {419659FD-72EA-4678-9EB8-B22A746CED70} + Debug + AnyCPU + WinExe + Properties + osu.Desktop + osu! + 3CF060CD28877D0E3112948951A64B2A7CEEC909 + codesigning.pfx + false + false + false + + + 3.5 + + + osu.Desktop.Program + OnOutputUpdated + false + LocalIntranet + v4.6.1 + true + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 2 + 1.0.0.%2a + false + true + 12.0.0 + 2.0 + + + + + + + true + full + false + bin\Debug\ + DEBUG + prompt + 0 + true + false + AnyCPU + true + false + false + false + + + + + none + true + bin\Release\ + CuttingEdge NoUpdate + prompt + 4 + true + false + AnyCPU + true + false + false + + + + + + + lazer.ico + + + Properties\app.manifest + + + true + bin\Debug\ + DEBUG + true + 0 + true + full + AnyCPU + false + prompt + --tests + + + + $(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.dll + True + + + $(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.MsDelta.dll + True + + + $(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.PatchApi.dll + True + + + $(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll + True + + + $(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll + True + + + $(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll + True + + + $(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Rocks.dll + True + + + + $(SolutionDir)\packages\squirrel.windows.1.7.8\lib\Net45\NuGet.Squirrel.dll + True + + + $(SolutionDir)\packages\OpenTK.3.0.0-git00009\lib\net20\OpenTK.dll + True + + + $(SolutionDir)\packages\SharpCompress.0.18.1\lib\net45\SharpCompress.dll + True + + + $(SolutionDir)\packages\Splat.2.0.0\lib\Net45\Splat.dll + True + + + $(SolutionDir)\packages\SQLitePCLRaw.bundle_green.1.1.8\lib\net45\SQLitePCLRaw.batteries_green.dll + + + $(SolutionDir)\packages\SQLitePCLRaw.bundle_green.1.1.8\lib\net45\SQLitePCLRaw.batteries_v2.dll + + + $(SolutionDir)\packages\SQLitePCLRaw.core.1.1.8\lib\net45\SQLitePCLRaw.core.dll + + + $(SolutionDir)\packages\SQLitePCLRaw.provider.e_sqlite3.net45.1.1.8\lib\net45\SQLitePCLRaw.provider.e_sqlite3.dll + + + $(SolutionDir)\packages\squirrel.windows.1.7.8\lib\Net45\Squirrel.dll + True + + + + + + $(SolutionDir)\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll + True + + + + + + + + + + + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 2.0 %28x86%29 + true + + + False + .NET Framework 3.0 %28x86%29 + false + + + False + .NET Framework 3.5 + false + + + False + .NET Framework 3.5 SP1 + false + + + + + + + + + + + + + + + {c76bf5b3-985e-4d39-95fe-97c9c879b83a} + osu.Framework + + + {d9a367c9-4c1a-489f-9b05-a0cea2b53b58} + osu.Game.Resources + + + {58f6c80c-1253-4a0e-a465-b8c85ebeadf3} + osu.Game.Rulesets.Catch + + + {48f4582b-7687-4621-9cbe-5c24197cb536} + osu.Game.Rulesets.Mania + + + {c92a607b-1fdd-4954-9f92-03ff547d9080} + osu.Game.Rulesets.Osu + + + {f167e17a-7de6-4af5-b920-a5112296c695} + osu.Game.Rulesets.Taiko + + + {54377672-20b1-40af-8087-5cf73bf3953a} + osu.Game.Tests + + + {2a66dd92-adb1-4994-89e2-c94e04acda0d} + osu.Game + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + \ No newline at end of file diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec new file mode 100644 index 0000000000..4c529f57e5 --- /dev/null +++ b/osu.Desktop/osu.nuspec @@ -0,0 +1,26 @@ + + + + osulazer + 0.0.0 + osulazer + ppy Pty Ltd + Dean Herbert + https://osu.ppy.sh/ + https://puu.sh/tYyXZ/9a01a5d1b0.ico + false + click the circles. to the beat. + click the circles. + testing + Copyright ppy Pty Ltd 2007-2017 + en-AU + + + + + + + + + + diff --git a/osu.Desktop/packages.config b/osu.Desktop/packages.config new file mode 100644 index 0000000000..6b6361b578 --- /dev/null +++ b/osu.Desktop/packages.config @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 2efb0c0707..6b9ec8b9a4 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -5,26 +5,49 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using System.Collections.Generic; using System; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu.UI; namespace osu.Game.Rulesets.Catch.Beatmaps { - internal class CatchBeatmapConverter : BeatmapConverter + internal class CatchBeatmapConverter : BeatmapConverter { protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; - protected override IEnumerable ConvertHitObject(HitObject obj, Beatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject obj, Beatmap beatmap) { - if (!(obj is IHasXPosition)) + var curveData = obj as IHasCurve; + var positionData = obj as IHasXPosition; + var comboData = obj as IHasCombo; + + if (positionData == null) yield break; + if (curveData != null) + { + yield return new JuiceStream + { + StartTime = obj.StartTime, + Samples = obj.Samples, + ControlPoints = curveData.ControlPoints, + CurveType = curveData.CurveType, + Distance = curveData.Distance, + RepeatSamples = curveData.RepeatSamples, + RepeatCount = curveData.RepeatCount, + X = positionData.X / CatchPlayfield.BASE_WIDTH, + NewCombo = comboData?.NewCombo ?? false + }; + + yield break; + } + yield return new Fruit { StartTime = obj.StartTime, - NewCombo = (obj as IHasCombo)?.NewCombo ?? false, - X = ((IHasXPosition)obj).X / OsuPlayfield.BASE_SIZE.X + Samples = obj.Samples, + NewCombo = comboData?.NewCombo ?? false, + X = positionData.X / CatchPlayfield.BASE_WIDTH }; } } diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 7fac19d135..9901dbde18 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -1,14 +1,20 @@ // 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.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects.Types; +using OpenTK; namespace osu.Game.Rulesets.Catch.Beatmaps { - internal class CatchBeatmapProcessor : BeatmapProcessor + internal class CatchBeatmapProcessor : BeatmapProcessor { - public override void PostProcess(Beatmap beatmap) + public override void PostProcess(Beatmap beatmap) { if (beatmap.ComboColors.Count == 0) return; @@ -16,7 +22,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int comboIndex = 0; int colourIndex = 0; - CatchBaseHit lastObj = null; + CatchHitObject lastObj = null; + + initialiseHyperDash(beatmap.HitObjects); foreach (var obj in beatmap.HitObjects) { @@ -34,5 +42,49 @@ namespace osu.Game.Rulesets.Catch.Beatmaps lastObj = obj; } } + + private void initialiseHyperDash(List objects) + { + // todo: add difficulty adjust. + double halfCatcherWidth = CatcherArea.CATCHER_SIZE * (objects.FirstOrDefault()?.Scale ?? 1) / CatchPlayfield.BASE_WIDTH / 2; + + int lastDirection = 0; + double lastExcess = halfCatcherWidth; + + int objCount = objects.Count; + + for (int i = 0; i < objCount - 1; i++) + { + CatchHitObject currentObject = objects[i]; + + // not needed? + // if (currentObject is TinyDroplet) continue; + + CatchHitObject nextObject = objects[i + 1]; + + // while (nextObject is TinyDroplet) + // { + // if (++i == objCount - 1) break; + // nextObject = objects[i + 1]; + // } + + int thisDirection = nextObject.X > currentObject.X ? 1 : -1; + double timeToNext = nextObject.StartTime - ((currentObject as IHasEndTime)?.EndTime ?? currentObject.StartTime) - 4; + double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); + + if (timeToNext * CatcherArea.Catcher.BASE_SPEED < distanceToNext) + { + currentObject.HyperDashTarget = nextObject; + lastExcess = halfCatcherWidth; + } + else + { + //currentObject.DistanceToHyperDash = timeToNext - distanceToNext; + lastExcess = MathHelper.Clamp(timeToNext - distanceToNext, 0, halfCatcherWidth); + } + + lastDirection = thisDirection; + } + } } } diff --git a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs index 3c6cc4b1a3..e9524a867d 100644 --- a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs @@ -8,17 +8,14 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.Catch { - public class CatchDifficultyCalculator : DifficultyCalculator + public class CatchDifficultyCalculator : DifficultyCalculator { public CatchDifficultyCalculator(Beatmap beatmap) : base(beatmap) { } - protected override double CalculateInternal(Dictionary categoryDifficulty) - { - return 0; - } + public override double Calculate(Dictionary categoryDifficulty = null) => 0; - protected override BeatmapConverter CreateBeatmapConverter() => new CatchBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter(); } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 1a9b034cf2..cb46c75583 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -93,9 +93,11 @@ namespace osu.Game.Rulesets.Catch public override string Description => "osu!catch"; + public override string ShortName => "fruits"; + public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new CatchDifficultyCalculator(beatmap); + public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap); public override int LegacyID => 2; diff --git a/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs b/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs deleted file mode 100644 index 2f33cf1093..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using OpenTK.Graphics; - -namespace osu.Game.Rulesets.Catch.Objects -{ - public abstract class CatchBaseHit : HitObject, IHasXPosition, IHasCombo - { - public float X { get; set; } - - public Color4 ComboColour { get; set; } = Color4.Gray; - public int ComboIndex { get; set; } - - public virtual bool NewCombo { get; set; } - - /// - /// The next fruit starts a new combo. Used for explodey. - /// - public virtual bool LastInCombo { get; set; } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs new file mode 100644 index 0000000000..9952e85c70 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects +{ + public abstract class CatchHitObject : HitObject, IHasXPosition, IHasCombo + { + public const double OBJECT_RADIUS = 44; + + public float X { get; set; } + + public Color4 ComboColour { get; set; } = Color4.Gray; + public int ComboIndex { get; set; } + + public virtual bool NewCombo { get; set; } + + /// + /// The next fruit starts a new combo. Used for explodey. + /// + public virtual bool LastInCombo { get; set; } + + public float Scale { get; set; } = 1; + + /// + /// Whether this fruit can initiate a hyperdash. + /// + public bool HyperDash => HyperDashTarget != null; + + /// + /// The target fruit if we are to initiate a hyperdash. + /// + public CatchHitObject HyperDashTarget; + + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + + Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs new file mode 100644 index 0000000000..b90a06b94e --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.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 osu.Framework.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; + +namespace osu.Game.Rulesets.Catch.Objects.Drawable +{ + public abstract class DrawableCatchHitObject : DrawableCatchHitObject + where TObject : CatchHitObject + { + public new TObject HitObject; + + protected DrawableCatchHitObject(TObject hitObject) + : base(hitObject) + { + HitObject = hitObject; + + Scale = new Vector2(HitObject.Scale); + } + } + + public abstract class DrawableCatchHitObject : DrawableScrollingHitObject + { + protected DrawableCatchHitObject(CatchHitObject hitObject) + : base(hitObject) + { + RelativePositionAxes = Axes.Both; + X = hitObject.X; + Y = (float)HitObject.StartTime; + } + + public Func CheckPosition; + + protected override void CheckForJudgements(bool userTriggered, double timeOffset) + { + if (timeOffset > 0) + AddJudgement(new Judgement { Result = CheckPosition?.Invoke(HitObject) ?? false ? HitResult.Perfect : HitResult.Miss }); + } + + private const float preempt = 1000; + + protected override void UpdateState(ArmedState state) + { + using (BeginAbsoluteSequence(HitObject.StartTime - preempt)) + { + // animation + this.FadeIn(200); + } + + switch (state) + { + case ArmedState.Miss: + using (BeginAbsoluteSequence(HitObject.StartTime, true)) + this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out); + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs new file mode 100644 index 0000000000..2b2a8e7f8d --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs @@ -0,0 +1,34 @@ +// 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.Catch.Objects.Drawable.Pieces; +using OpenTK; + +namespace osu.Game.Rulesets.Catch.Objects.Drawable +{ + public class DrawableDroplet : DrawableCatchHitObject + { + public DrawableDroplet(Droplet h) + : base(h) + { + Origin = Anchor.Centre; + + Size = new Vector2(Pulp.PULP_SIZE); + + AccentColour = h.ComboColour; + Masking = false; + } + + [BackgroundDependencyLoader] + private void load() + { + Child = new Pulp + { + AccentColour = AccentColour, + Scale = new Vector2(0.8f), + }; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs index e0c9f0c028..9f46bbd3a4 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs @@ -1,72 +1,30 @@ // 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.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.MathUtils; -using osu.Game.Graphics; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces; using OpenTK; using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawable { - public class DrawableFruit : DrawableScrollingHitObject + public class DrawableFruit : DrawableCatchHitObject { - private const float pulp_size = 20; - - private class Pulp : Circle, IHasAccentColour - { - public Pulp() - { - Size = new Vector2(pulp_size); - - Blending = BlendingMode.Additive; - Colour = Color4.White.Opacity(0.9f); - } - - private Color4 accentColour; - public Color4 AccentColour - { - get { return accentColour; } - set - { - accentColour = value; - - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = accentColour.Lighten(100), - }; - } - } - } - - - public DrawableFruit(CatchBaseHit h) + public DrawableFruit(Fruit h) : base(h) { Origin = Anchor.Centre; - Size = new Vector2(pulp_size * 2.2f, pulp_size * 2.8f); - - RelativePositionAxes = Axes.Both; - X = h.X; + Size = new Vector2(Pulp.PULP_SIZE * 2.2f, Pulp.PULP_SIZE * 2.8f); AccentColour = HitObject.ComboColour; - Masking = false; Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; } - public Func CheckPosition; - [BackgroundDependencyLoader] private void load() { @@ -113,30 +71,19 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable } } }; - } - private const float preempt = 1000; - - protected override void CheckForJudgements(bool userTriggered, double timeOffset) - { - if (timeOffset > 0) - AddJudgement(new Judgement { Result = CheckPosition?.Invoke(HitObject) ?? false ? HitResult.Perfect : HitResult.Miss }); - } - - protected override void UpdateState(ArmedState state) - { - using (BeginAbsoluteSequence(HitObject.StartTime - preempt)) + if (HitObject.HyperDash) { - // animation - this.FadeIn(200); - } - - switch (state) - { - case ArmedState.Miss: - using (BeginAbsoluteSequence(HitObject.StartTime, true)) - this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out); - break; + Add(new Pulp + { + RelativePositionAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AccentColour = Color4.Red, + Blending = BlendingMode.Additive, + Alpha = 0.5f, + Scale = new Vector2(2) + }); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs new file mode 100644 index 0000000000..db0632a07d --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.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 System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using OpenTK; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Catch.Objects.Drawable +{ + public class DrawableJuiceStream : DrawableCatchHitObject + { + private readonly Container dropletContainer; + + public DrawableJuiceStream(JuiceStream s) : base(s) + { + RelativeSizeAxes = Axes.Both; + Height = (float)HitObject.Duration; + X = 0; + + Child = dropletContainer = new Container + { + RelativeSizeAxes = Axes.Both, + RelativeChildOffset = new Vector2(0, (float)HitObject.StartTime), + RelativeChildSize = new Vector2(1, (float)HitObject.Duration) + }; + + foreach (CatchHitObject tick in s.NestedHitObjects.OfType()) + { + TinyDroplet tiny = tick as TinyDroplet; + if (tiny != null) + { + AddNested(new DrawableDroplet(tiny) { Scale = new Vector2(0.5f) }); + continue; + } + + Droplet droplet = tick as Droplet; + if (droplet != null) + AddNested(new DrawableDroplet(droplet)); + + Fruit fruit = tick as Fruit; + if (fruit != null) + AddNested(new DrawableFruit(fruit)); + } + } + + protected override void AddNested(DrawableHitObject h) + { + ((DrawableCatchHitObject)h).CheckPosition = o => CheckPosition?.Invoke(o) ?? false; + dropletContainer.Add(h); + base.AddNested(h); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs new file mode 100644 index 0000000000..2de266b3f0 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces +{ + public class Pulp : Circle, IHasAccentColour + { + public const float PULP_SIZE = (float)CatchHitObject.OBJECT_RADIUS / 2.2f; + + public Pulp() + { + Size = new Vector2(PULP_SIZE); + + Blending = BlendingMode.Additive; + Colour = Color4.White.Opacity(0.9f); + } + + private Color4 accentColour; + public Color4 AccentColour + { + get { return accentColour; } + set + { + accentColour = value; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = accentColour.Lighten(100), + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs index b1206e0d75..a2bdf830e5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Catch.Objects { - public class Droplet : CatchBaseHit + public class Droplet : CatchHitObject { } } diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs index fc55f83969..5f1060fb51 100644 --- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Catch.Objects { - public class Fruit : CatchBaseHit + public class Fruit : CatchHitObject { } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs new file mode 100644 index 0000000000..8e496c3b0c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -0,0 +1,170 @@ +// 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.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using OpenTK; + +namespace osu.Game.Rulesets.Catch.Objects +{ + public class JuiceStream : CatchHitObject, IHasCurve + { + /// + /// Positional distance that results in a duration of one second, before any speed adjustments. + /// + private const float base_scoring_distance = 100; + + public readonly SliderCurve Curve = new SliderCurve(); + + public int RepeatCount { get; set; } = 1; + + public double Velocity; + public double TickDistance; + + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + + TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); + DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); + + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + + Velocity = scoringDistance / timingPoint.BeatLength; + TickDistance = scoringDistance / difficulty.SliderTickRate; + } + + protected override void CreateNestedHitObjects() + { + base.CreateNestedHitObjects(); + + createTicks(); + } + + private void createTicks() + { + if (TickDistance == 0) + return; + + var length = Curve.Distance; + var tickDistance = Math.Min(TickDistance, length); + var repeatDuration = length / Velocity; + + var minDistanceFromEnd = Velocity * 0.01; + + AddNested(new Fruit + { + Samples = Samples, + ComboColour = ComboColour, + StartTime = StartTime, + X = X + }); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + { + var repeatStartTime = StartTime + repeat * repeatDuration; + var reversed = repeat % 2 == 1; + + for (var d = tickDistance; d <= length; d += tickDistance) + { + if (d > length - minDistanceFromEnd) + break; + + var timeProgress = d / length; + var distanceProgress = reversed ? 1 - timeProgress : timeProgress; + + var lastTickTime = repeatStartTime + timeProgress * repeatDuration; + AddNested(new Droplet + { + StartTime = lastTickTime, + ComboColour = ComboColour, + X = Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH, + Samples = new SampleInfoList(Samples.Select(s => new SampleInfo + { + Bank = s.Bank, + Name = @"slidertick", + Volume = s.Volume + })) + }); + } + + double tinyTickInterval = tickDistance / length * repeatDuration; + while (tinyTickInterval > 100) + tinyTickInterval /= 2; + + for (double t = 0; t < repeatDuration; t += tinyTickInterval) + { + double progress = reversed ? 1 - t / repeatDuration : t / repeatDuration; + + AddNested(new TinyDroplet + { + StartTime = repeatStartTime + t, + ComboColour = ComboColour, + X = Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, + Samples = new SampleInfoList(Samples.Select(s => new SampleInfo + { + Bank = s.Bank, + Name = @"slidertick", + Volume = s.Volume + })) + }); + } + + AddNested(new Fruit + { + Samples = Samples, + ComboColour = ComboColour, + StartTime = repeatStartTime + repeatDuration, + X = Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH + }); + } + + } + + + public double EndTime => StartTime + RepeatCount * Curve.Distance / Velocity; + + public float EndX => Curve.PositionAt(ProgressAt(1)).X / CatchPlayfield.BASE_WIDTH; + + public double Duration => EndTime - StartTime; + + public double Distance + { + get { return Curve.Distance; } + set { Curve.Distance = value; } + } + + public List ControlPoints + { + get { return Curve.ControlPoints; } + set { Curve.ControlPoints = value; } + } + + public List RepeatSamples { get; set; } = new List(); + + public CurveType CurveType + { + get { return Curve.CurveType; } + set { Curve.CurveType = value; } + } + + public Vector2 PositionAt(double progress) => Curve.PositionAt(ProgressAt(progress)); + + public double ProgressAt(double progress) + { + double p = progress * RepeatCount % 1; + if (RepeatAt(progress) % 2 == 1) + p = 1 - p; + return p; + } + + public int RepeatAt(double progress) => (int)(progress * RepeatCount); + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs new file mode 100644 index 0000000000..231f3d5361 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs @@ -0,0 +1,9 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Catch.Objects +{ + public class TinyDroplet : Droplet + { + } +} diff --git a/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs index 42fbc7e082..dd2006c60c 100644 --- a/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs +++ b/osu.Game.Rulesets.Catch/Properties/AssemblyInfo.cs @@ -8,11 +8,11 @@ using System.Runtime.InteropServices; // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("osu.Game.Rulesets.Catch")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyDescription("catch the fruit. to the beat.")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] +[assembly: AssemblyCompany("ppy Pty Ltd")] [assembly: AssemblyProduct("osu.Game.Rulesets.Catch")] -[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyCopyright("ppy Pty Ltd 2007-2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -24,15 +24,5 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("58f6c80c-1253-4a0e-a465-b8c85ebeadf3")] -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 16756e65f1..3826fd1129 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.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.Game.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; @@ -10,17 +11,30 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.Scoring { - internal class CatchScoreProcessor : ScoreProcessor + internal class CatchScoreProcessor : ScoreProcessor { - public CatchScoreProcessor(RulesetContainer rulesetContainer) + public CatchScoreProcessor(RulesetContainer rulesetContainer) : base(rulesetContainer) { } - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void SimulateAutoplay(Beatmap beatmap) { foreach (var obj in beatmap.HitObjects) { + var stream = obj as JuiceStream; + + if (stream != null) + { + AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); + AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); + + foreach (var unused in stream.NestedHitObjects.OfType()) + AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); + + continue; + } + var fruit = obj as Fruit; if (fruit != null) diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs index 8d18a712d8..cc434b3080 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs @@ -2,22 +2,15 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using NUnit.Framework; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Catch.Objects; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] + [Ignore("getting CI working")] public class TestCaseCatchPlayer : Game.Tests.Visual.TestCasePlayer { - protected override Beatmap CreateBeatmap() + public TestCaseCatchPlayer() : base(typeof(CatchRuleset)) { - var beatmap = new Beatmap(); - - for (int i = 0; i < 256; i++) - beatmap.HitObjects.Add(new Fruit { X = 0.5f, StartTime = i * 100, NewCombo = i % 8 == 0 }); - - return beatmap; } } } diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs new file mode 100644 index 0000000000..586de17f15 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + [Ignore("getting CI working")] + public class TestCaseCatchStacker : Game.Tests.Visual.TestCasePlayer + { + public TestCaseCatchStacker() + : base(typeof(CatchRuleset)) + { + } + + protected override Beatmap CreateBeatmap() + { + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 6, + } + } + }; + + for (int i = 0; i < 512; i++) + beatmap.HitObjects.Add(new Fruit { X = 0.5f + i / 2048f * (i % 10 - 5), StartTime = i * 100, NewCombo = i % 8 == 0 }); + + return beatmap; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcher.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcher.cs deleted file mode 100644 index 6a065e197d..0000000000 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcher.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; -using System.Collections.Generic; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Tests.Visual; -using OpenTK; - -namespace osu.Game.Rulesets.Catch.Tests -{ - [TestFixture] - internal class TestCaseCatcher : OsuTestCase - { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(CatcherArea), - }; - - [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.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs new file mode 100644 index 0000000000..8217265e3d --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.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 System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + [Ignore("getting CI working")] + public class TestCaseCatcherArea : OsuTestCase + { + private RulesetInfo catchRuleset; + private TestCatcherArea catcherArea; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(CatcherArea), + }; + + public TestCaseCatcherArea() + { + AddSliderStep("CircleSize", 0, 8, 5, createCatcher); + AddToggleStep("Hyperdash", t => catcherArea.ToggleHyperDash(t)); + } + + private void createCatcher(float size) + { + Child = new CatchInputManager(catchRuleset) + { + RelativeSizeAxes = Axes.Both, + Child = catcherArea = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size }) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.BottomLeft + }, + }; + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + catchRuleset = rulesets.GetRuleset(2); + } + + private class TestCatcherArea : CatcherArea + { + public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) + : base(beatmapDifficulty) + { + } + + public void ToggleHyperDash(bool status) => MovableCatcher.HyperDashModifier = status ? 2 : 1; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs new file mode 100644 index 0000000000..ce3f79bae2 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs @@ -0,0 +1,30 @@ +// 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.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + [Ignore("getting CI working")] + public class TestCaseHyperdash : Game.Tests.Visual.TestCasePlayer + { + public TestCaseHyperdash() + : base(typeof(CatchRuleset)) + { + } + + protected override Beatmap CreateBeatmap() + { + var beatmap = new Beatmap(); + + for (int i = 0; i < 512; i++) + if (i % 5 < 3) + beatmap.HitObjects.Add(new Fruit { X = i % 10 < 5 ? 0.02f : 0.98f, StartTime = i * 100, NewCombo = i % 8 == 0 }); + + return beatmap; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..0d2dc14160 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [Ignore("getting CI working")] + public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints + { + public TestCasePerformancePoints() + : base(new CatchRuleset(new RulesetInfo())) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 2b6f9bbf5a..76dbfa77c6 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -5,6 +5,8 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.UI; using OpenTK; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; @@ -13,16 +15,19 @@ namespace osu.Game.Rulesets.Catch.UI { public class CatchPlayfield : ScrollingPlayfield { + public const float BASE_WIDTH = 512; + protected override Container Content => content; private readonly Container content; + private readonly CatcherArea catcherArea; - public CatchPlayfield() + public CatchPlayfield(BeatmapDifficulty difficulty) : base(Axes.Y) { - Reversed.Value = true; + Container explodingFruitContainer; - Size = new Vector2(1); + Reversed.Value = true; Anchor = Anchor.TopCentre; Origin = Anchor.TopCentre; @@ -33,24 +38,29 @@ namespace osu.Game.Rulesets.Catch.UI { RelativeSizeAxes = Axes.Both, }, - catcherArea = new CatcherArea + explodingFruitContainer = new Container { RelativeSizeAxes = Axes.Both, + }, + catcherArea = new CatcherArea(difficulty) + { + ExplodingFruitTarget = explodingFruitContainer, Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, - Height = 0.3f } }; } + public bool CheckIfWeCanCatch(CatchHitObject obj) => catcherArea.AttemptCatch(obj); + public override void Add(DrawableHitObject h) { h.Depth = (float)h.HitObject.StartTime; base.Add(h); - var fruit = (DrawableFruit)h; - fruit.CheckPosition = catcherArea.CheckIfWeCanCatch; + var fruit = (DrawableCatchHitObject)h; + fruit.CheckPosition = CheckIfWeCanCatch; } public override void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) @@ -58,7 +68,11 @@ namespace osu.Game.Rulesets.Catch.UI if (judgement.IsHit) { Vector2 screenPosition = judgedObject.ScreenSpaceDrawQuad.Centre; - Remove(judgedObject); + + // todo: don't do this + (judgedObject.Parent as Container)?.Remove(judgedObject); + (judgedObject.Parent as Container)?.Remove(judgedObject); + catcherArea.Add(judgedObject, screenPosition); } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 8a6ef71996..3ed9090098 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.UI { - public class CatchRulesetContainer : ScrollingRulesetContainer + public class CatchRulesetContainer : ScrollingRulesetContainer { public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) : base(ruleset, beatmap, isForCurrentRuleset) @@ -22,18 +22,23 @@ namespace osu.Game.Rulesets.Catch.UI public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this); - protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); + protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); - protected override BeatmapConverter CreateBeatmapConverter() => new CatchBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new CatchBeatmapConverter(); - protected override Playfield CreatePlayfield() => new CatchPlayfield(); + protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); - protected override DrawableHitObject GetVisualRepresentation(CatchBaseHit h) + protected override DrawableHitObject GetVisualRepresentation(CatchHitObject h) { - if (h is Fruit) - return new DrawableFruit(h); + var fruit = h as Fruit; + if (fruit != null) + return new DrawableFruit(fruit); + + var stream = h as JuiceStream; + if (stream != null) + return new DrawableJuiceStream(stream); return null; } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 2930dbb7cc..2bb0f3cd18 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -10,53 +10,73 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.MathUtils; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects.Drawables; using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { public class CatcherArea : Container { - private Catcher catcher; - private Container explodingFruitContainer; + public const float CATCHER_SIZE = 172; - public void Add(DrawableHitObject fruit, Vector2 screenPosition) => catcher.AddToStack(fruit, screenPosition); + protected readonly Catcher MovableCatcher; - public bool CheckIfWeCanCatch(CatchBaseHit obj) => Math.Abs(catcher.Position.X - obj.X) < catcher.DrawSize.X / DrawSize.X / 2; - - [BackgroundDependencyLoader] - private void load() + public Container ExplodingFruitTarget { - Children = new Drawable[] + set { MovableCatcher.ExplodingFruitTarget = value; } + } + + public CatcherArea(BeatmapDifficulty difficulty = null) + { + RelativeSizeAxes = Axes.X; + Height = CATCHER_SIZE; + Child = MovableCatcher = new Catcher(difficulty) { - explodingFruitContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - catcher = new Catcher - { - RelativePositionAxes = Axes.Both, - ExplodingFruitTarget = explodingFruitContainer, - Origin = Anchor.TopCentre, - X = 0.5f, - } + AdditiveTarget = this, }; } - protected override void Update() + public void Add(DrawableHitObject fruit, Vector2 absolutePosition) { - base.Update(); + fruit.RelativePositionAxes = Axes.None; + fruit.Position = new Vector2(MovableCatcher.ToLocalSpace(absolutePosition).X - MovableCatcher.DrawSize.X / 2, 0); - catcher.Size = new Vector2(DrawSize.Y); + fruit.Anchor = Anchor.TopCentre; + fruit.Origin = Anchor.BottomCentre; + fruit.Scale *= 0.7f; + fruit.LifetimeEnd = double.MaxValue; + + MovableCatcher.Add(fruit); } - private class Catcher : Container, IKeyBindingHandler + public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj); + + public class Catcher : Container, IKeyBindingHandler { private Texture texture; private Container caughtFruit; + public Container ExplodingFruitTarget; + + public Container AdditiveTarget; + + public Catcher(BeatmapDifficulty difficulty = null) + { + RelativePositionAxes = Axes.X; + X = 0.5f; + + Origin = Anchor.TopCentre; + Anchor = Anchor.TopLeft; + + Size = new Vector2(CATCHER_SIZE); + if (difficulty != null) + Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } + [BackgroundDependencyLoader] private void load(TextureStore textures) { @@ -77,8 +97,6 @@ namespace osu.Game.Rulesets.Catch.UI private bool dashing; - public Container ExplodingFruitTarget; - protected bool Dashing { get { return dashing; } @@ -88,37 +106,151 @@ namespace osu.Game.Rulesets.Catch.UI dashing = value; - if (dashing) - Schedule(addAdditiveSprite); + Trail |= dashing; } } - private void addAdditiveSprite() + private bool trail; + + /// + /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. + /// + protected bool Trail { - if (!dashing) return; + get { return trail; } + set + { + if (value == trail) return; + + trail = value; + + if (Trail) + beginTrail(); + } + } + + private void beginTrail() + { + Trail &= dashing || HyperDashing; + Trail &= AdditiveTarget != null; + + if (!Trail) return; var additive = createCatcherSprite(); - additive.RelativePositionAxes = Axes.Both; - additive.Blending = BlendingMode.Additive; + additive.Anchor = Anchor; + additive.OriginPosition = additive.OriginPosition + new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly. additive.Position = Position; additive.Scale = Scale; + additive.Colour = HyperDashing ? Color4.Red : Color4.White; + additive.RelativePositionAxes = RelativePositionAxes; + additive.Blending = BlendingMode.Additive; - ((CatcherArea)Parent).Add(additive); + AdditiveTarget.Add(additive); additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint).Expire(); - Scheduler.AddDelayed(addAdditiveSprite, 50); + Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); } private Sprite createCatcherSprite() => new Sprite { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, + Size = new Vector2(CATCHER_SIZE), + FillMode = FillMode.Fill, Texture = texture, - OriginPosition = new Vector2(DrawWidth / 2, 10) //temporary until the sprite is aligned correctly. + OriginPosition = new Vector2(-3, 10) // temporary until the sprite is aligned correctly. }; + /// + /// Add a caught fruit to the catcher's stack. + /// + /// The fruit that was caught. + public void Add(DrawableHitObject fruit) + { + float distance = fruit.DrawSize.X / 2 * fruit.Scale.X; + + while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance)) + { + fruit.X += RNG.Next(-5, 5); + fruit.Y -= RNG.Next(0, 5); + } + + caughtFruit.Add(fruit); + + var catchObject = (CatchHitObject)fruit.HitObject; + + if (catchObject.LastInCombo) + explode(); + } + + /// + /// Let the catcher attempt to catch a fruit. + /// + /// The fruit to catch. + /// Whether the catch is possible. + public bool AttemptCatch(CatchHitObject fruit) + { + const double relative_catcher_width = CATCHER_SIZE / 2; + + // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. + var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; + var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; + + var validCatch = + catchObjectPosition >= catcherPosition - relative_catcher_width / 2 && + catchObjectPosition <= catcherPosition + relative_catcher_width / 2; + + if (validCatch && fruit.HyperDash) + { + HyperDashModifier = Math.Abs(fruit.HyperDashTarget.X - fruit.X) / Math.Abs(fruit.HyperDashTarget.StartTime - fruit.StartTime) / BASE_SPEED; + HyperDashDirection = fruit.HyperDashTarget.X - fruit.X; + } + else + HyperDashModifier = 1; + + return validCatch; + } + + /// + /// Whether we are hypderdashing or not. + /// + public bool HyperDashing => hyperDashModifier != 1; + + private double hyperDashModifier = 1; + + /// + /// The direction in which hyperdash is allowed. 0 allows both directions. + /// + public double HyperDashDirection; + + /// + /// The speed modifier resultant from hyperdash. Will trigger hyperdash when not equal to 1. + /// + public double HyperDashModifier + { + get { return hyperDashModifier; } + set + { + if (value == hyperDashModifier) return; + hyperDashModifier = value; + + const float transition_length = 180; + + if (HyperDashing) + { + this.FadeColour(Color4.OrangeRed, transition_length, Easing.OutQuint); + this.FadeTo(0.2f, transition_length, Easing.OutQuint); + Trail = true; + } + else + { + HyperDashDirection = 0; + this.FadeColour(Color4.White, transition_length, Easing.OutQuint); + this.FadeTo(1, transition_length, Easing.OutQuint); + } + } + } + public bool OnPressed(CatchAction action) { switch (action) @@ -158,7 +290,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. /// - private const double base_speed = 1.0 / 512; + public const double BASE_SPEED = 1.0 / 512; protected override void Update() { @@ -166,34 +298,15 @@ namespace osu.Game.Rulesets.Catch.UI if (currentDirection == 0) return; + var direction = Math.Sign(currentDirection); + double dashModifier = Dashing ? 1 : 0.5; - Scale = new Vector2(Math.Sign(currentDirection), 1); - X = (float)MathHelper.Clamp(X + Math.Sign(currentDirection) * Clock.ElapsedFrameTime * base_speed * dashModifier, 0, 1); - } + if (hyperDashModifier != 1 && (HyperDashDirection == 0 || direction == Math.Sign(HyperDashDirection))) + dashModifier = hyperDashModifier; - public void AddToStack(DrawableHitObject fruit, Vector2 absolutePosition) - { - fruit.RelativePositionAxes = Axes.None; - fruit.Position = new Vector2(ToLocalSpace(absolutePosition).X - DrawSize.X / 2, 0); - - fruit.Anchor = Anchor.TopCentre; - fruit.Origin = Anchor.BottomCentre; - fruit.Scale *= 0.7f; - fruit.LifetimeEnd = double.MaxValue; - - float distance = fruit.DrawSize.X / 2 * fruit.Scale.X; - - while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance)) - { - fruit.X += RNG.Next(-5, 5); - fruit.Y -= RNG.Next(0, 5); - } - - caughtFruit.Add(fruit); - - if (((CatchBaseHit)fruit.HitObject).LastInCombo) - explode(); + Scale = new Vector2(Math.Abs(Scale.X) * direction, Scale.Y); + X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * BASE_SPEED * dashModifier, 0, 1); } private void explode() @@ -215,8 +328,8 @@ namespace osu.Game.Rulesets.Catch.UI } f.MoveToY(f.Y - 50, 250, Easing.OutSine) - .Then() - .MoveToY(f.Y + 50, 500, Easing.InSine); + .Then() + .MoveToY(f.Y + 50, 500, Easing.InSine); f.MoveToX(f.X + originalX * 6, 1000); f.FadeOut(750); diff --git a/osu.Game.Rulesets.Catch/app.config b/osu.Game.Rulesets.Catch/app.config index 11af32e2cf..c9d4e44b1a 100644 --- a/osu.Game.Rulesets.Catch/app.config +++ b/osu.Game.Rulesets.Catch/app.config @@ -10,6 +10,10 @@ + + + + \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj index 787825d482..b03c8d2eea 100644 --- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj +++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -16,17 +16,16 @@ true full false - ..\osu.Game\bin\Debug\ + bin\Debug\ DEBUG;TRACE prompt 4 false - 6 pdbonly true - ..\osu.Game\bin\Release\ + bin\Release\ TRACE prompt 4 @@ -35,11 +34,11 @@ $(SolutionDir)\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll - False + True $(SolutionDir)\packages\OpenTK.3.0.0-git00009\lib\net20\OpenTK.dll - False + True @@ -50,15 +49,24 @@ + + + + + - + + - + + + + @@ -66,9 +74,6 @@ - - osu.licenseheader - @@ -77,19 +82,17 @@ {C76BF5B3-985E-4D39-95FE-97C9C879B83A} osu.Framework - False - - - {C92A607B-1FDD-4954-9F92-03FF547D9080} - osu.Game.Rulesets.Osu - False + True {2a66dd92-adb1-4994-89e2-c94e04acda0d} osu.Game - False + True + + + - - - - - + + + + + + + \ No newline at end of file diff --git a/osu.Game.props b/osu.Game.props new file mode 100644 index 0000000000..60a5e97944 --- /dev/null +++ b/osu.Game.props @@ -0,0 +1,13 @@ + + + + + + 7 + + + + osu.licenseheader + + + \ No newline at end of file diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 171a1bdf75..64a9aa50a0 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -1,8 +1,12 @@ // 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.Sample; + namespace osu.Game.Audio { + [Serializable] public class SampleInfo { public const string HIT_WHISTLE = @"hitwhistle"; @@ -10,6 +14,13 @@ namespace osu.Game.Audio public const string HIT_NORMAL = @"hitnormal"; public const string HIT_CLAP = @"hitclap"; + public SampleChannel GetChannel(SampleManager manager) + { + var channel = manager.Get($"Gameplay/{Bank}-{Name}"); + channel.Volume.Value = Volume / 100.0; + return channel; + } + /// /// The bank to load the sample from. /// diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 458c2304f2..b639de640a 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -8,20 +8,21 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; using osu.Game.IO.Serialization; -using osu.Game.Storyboards; +using Newtonsoft.Json; +using osu.Game.IO.Serialization.Converters; namespace osu.Game.Beatmaps { /// /// A Beatmap containing converted HitObjects. /// - public class Beatmap + public class Beatmap : IJsonSerializable where T : HitObject { public BeatmapInfo BeatmapInfo = new BeatmapInfo(); public ControlPointInfo ControlPointInfo = new ControlPointInfo(); public List Breaks = new List(); - public readonly List ComboColors = new List + public List ComboColors = new List { new Color4(17, 136, 170, 255), new Color4(102, 136, 0, 255), @@ -29,23 +30,21 @@ namespace osu.Game.Beatmaps new Color4(121, 9, 13, 255) }; + [JsonIgnore] public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata; /// /// The HitObjects this Beatmap contains. /// + [JsonConverter(typeof(TypedListConverter))] public List HitObjects = new List(); /// /// Total amount of break time in the beatmap. /// + [JsonIgnore] public double TotalBreakTime => Breaks.Sum(b => b.Duration); - /// - /// The Beatmap's Storyboard. - /// - public Storyboard Storyboard = new Storyboard(); - /// /// Constructs a new beatmap. /// @@ -57,7 +56,23 @@ namespace osu.Game.Beatmaps Breaks = original?.Breaks ?? Breaks; ComboColors = original?.ComboColors ?? ComboColors; HitObjects = original?.HitObjects ?? HitObjects; - Storyboard = original?.Storyboard ?? Storyboard; + + if (original == null && Metadata == null) + { + // we may have no metadata in cases we weren't sourced from the database. + // let's fill it (and other related fields) so we don't need to null-check it in future usages. + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = @"Unknown", + Title = @"Unknown", + AuthorString = @"Unknown Creator", + }, + Version = @"Normal", + BaseDifficulty = new BeatmapDifficulty() + }; + } } } diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 962c790fb2..e087eebbfe 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -80,6 +80,7 @@ namespace osu.Game.Beatmaps /// /// Performs the conversion of a hit object. + /// This method is generally executed sequentially for all objects in a beatmap. /// /// The hit object to convert. /// The un-converted Beatmap. diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 7c2294cae9..03fbf9a0a7 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -1,7 +1,8 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using SQLite.Net.Attributes; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; namespace osu.Game.Beatmaps { @@ -12,8 +13,10 @@ namespace osu.Game.Beatmaps /// public const float DEFAULT_DIFFICULTY = 5; - [PrimaryKey, AutoIncrement] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [JsonIgnore] public int ID { get; set; } + public float DrainRate { get; set; } = DEFAULT_DIFFICULTY; public float CircleSize { get; set; } = DEFAULT_DIFFICULTY; public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY; diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 5e4e122fb5..1da3dc8a54 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -2,49 +2,62 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; +using osu.Game.Database; using osu.Game.IO.Serialization; using osu.Game.Rulesets; -using SQLite.Net.Attributes; -using SQLiteNetExtensions.Attributes; namespace osu.Game.Beatmaps { - public class BeatmapInfo : IEquatable, IJsonSerializable + [Serializable] + public class BeatmapInfo : IEquatable, IJsonSerializable, IHasPrimaryKey { - [PrimaryKey, AutoIncrement] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [JsonIgnore] public int ID { get; set; } //TODO: should be in database public int BeatmapVersion; - public int? OnlineBeatmapID { get; set; } + private int? onlineBeatmapID; + private int? onlineBeatmapSetID; - public int? OnlineBeatmapSetID { get; set; } + [JsonProperty("id")] + public int? OnlineBeatmapID + { + get { return onlineBeatmapID; } + set { onlineBeatmapID = value > 0 ? value : null; } + } - [ForeignKey(typeof(BeatmapSetInfo))] + [JsonProperty("beatmapset_id")] + [NotMapped] + public int? OnlineBeatmapSetID + { + get { return onlineBeatmapSetID; } + set { onlineBeatmapSetID = value > 0 ? value : null; } + } + + [JsonIgnore] public int BeatmapSetInfoID { get; set; } - [ManyToOne] + [Required] + [JsonIgnore] public BeatmapSetInfo BeatmapSet { get; set; } - [ForeignKey(typeof(BeatmapMetadata))] - public int BeatmapMetadataID { get; set; } - - [OneToOne(CascadeOperations = CascadeOperation.All)] public BeatmapMetadata Metadata { get; set; } - [ForeignKey(typeof(BeatmapDifficulty)), NotNull] + [JsonIgnore] public int BaseDifficultyID { get; set; } - [OneToOne(CascadeOperations = CascadeOperation.All)] - public BeatmapDifficulty Difficulty { get; set; } + public BeatmapDifficulty BaseDifficulty { get; set; } - [Ignore] + [NotMapped] public BeatmapMetrics Metrics { get; set; } - [Ignore] + [NotMapped] public BeatmapOnlineInfo OnlineInfo { get; set; } public string Path { get; set; } @@ -52,12 +65,12 @@ namespace osu.Game.Beatmaps [JsonProperty("file_sha2")] public string Hash { get; set; } + [JsonIgnore] public bool Hidden { get; set; } /// /// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.). /// - [Indexed] [JsonProperty("file_md5")] public string MD5Hash { get; set; } @@ -67,10 +80,8 @@ namespace osu.Game.Beatmaps public float StackLeniency { get; set; } public bool SpecialStyle { get; set; } - [ForeignKey(typeof(RulesetInfo))] public int RulesetID { get; set; } - [OneToOne(CascadeOperations = CascadeOperation.CascadeRead)] public RulesetInfo Ruleset { get; set; } public bool LetterboxInBreaks { get; set; } @@ -99,7 +110,7 @@ namespace osu.Game.Beatmaps } } - [Ignore] + [NotMapped] public int[] Bookmarks { get; set; } = new int[0]; public double DistanceSpacing { get; set; } @@ -110,8 +121,11 @@ namespace osu.Game.Beatmaps // Metadata public string Version { get; set; } + [JsonProperty("difficulty_rating")] public double StarDifficulty { get; set; } + public override string ToString() => $"{Metadata} [{Version}]"; + public bool Equals(BeatmapInfo other) { if (ID == 0 || other?.ID == 0) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a1b678392b..0325785016 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Threading.Tasks; using Ionic.Zip; +using Microsoft.EntityFrameworkCore; using osu.Framework.Audio.Track; using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; @@ -15,14 +17,15 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.IO; +using osu.Game.Database; +using osu.Game.Graphics; using osu.Game.IO; using osu.Game.IPC; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; -using SQLite.Net; -using osu.Game.Online.API.Requests; -using System.Threading.Tasks; -using osu.Game.Online.API; +using osu.Game.Storyboards; namespace osu.Game.Beatmaps { @@ -51,6 +54,11 @@ namespace osu.Game.Beatmaps /// public event Action BeatmapRestored; + /// + /// Fired when a beatmap download begins. + /// + public event Action BeatmapDownloadBegan; + /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. /// @@ -58,9 +66,19 @@ namespace osu.Game.Beatmaps private readonly Storage storage; - private readonly FileStore files; + private BeatmapStore createBeatmapStore(Func context) + { + var store = new BeatmapStore(context); + store.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); + store.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); + store.BeatmapHidden += b => BeatmapHidden?.Invoke(b); + store.BeatmapRestored += b => BeatmapRestored?.Invoke(b); + return store; + } - private readonly SQLiteConnection connection; + private readonly Func createContext; + + private readonly FileStore files; private readonly RulesetStore rulesets; @@ -83,22 +101,27 @@ namespace osu.Game.Beatmaps /// public Func GetStableStorage { private get; set; } - public BeatmapManager(Storage storage, FileStore files, SQLiteConnection connection, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) + public BeatmapManager(Storage storage, Func context, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) { - beatmaps = new BeatmapStore(connection); - beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); - beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); - beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); - beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); + createContext = context; + importContext = new Lazy(() => + { + var c = createContext(); + c.Database.AutoTransactionsEnabled = false; + return c; + }); - this.storage = storage; - this.files = files; - this.connection = connection; + beatmaps = createBeatmapStore(context); + files = new FileStore(context, storage); + + this.storage = files.Storage; this.rulesets = rulesets; this.api = api; if (importHost != null) ipc = new BeatmapIPCChannel(importHost, this); + + beatmaps.Cleanup(); } /// @@ -111,6 +134,7 @@ namespace osu.Game.Beatmaps var notification = new ProgressNotification { Text = "Beatmap import is initialising...", + CompletionText = "Import successful!", Progress = 0, State = ProgressNotificationState.Active, }; @@ -149,14 +173,14 @@ namespace osu.Game.Beatmaps catch (Exception e) { e = e.InnerException ?? e; - Logger.Error(e, @"Could not import beatmap set"); + Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})"); } } notification.State = ProgressNotificationState.Completed; } - private readonly object importLock = new object(); + private readonly Lazy importContext; /// /// Import a beatmap from an . @@ -164,13 +188,29 @@ namespace osu.Game.Beatmaps /// 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))); + lock (importContext) + { + var context = importContext.Value; - return set; + using (var transaction = context.BeginTransaction()) + { + // create local stores so we can isolate and thread safely, and share a context/transaction. + var iFiles = new FileStore(() => context, storage); + var iBeatmaps = createBeatmapStore(() => context); + + BeatmapSetInfo set = importToStorage(iFiles, iBeatmaps, archiveReader); + + if (set.ID == 0) + { + iBeatmaps.Add(set); + context.SaveChanges(); + } + + context.SaveChanges(transaction); + return set; + } + } } /// @@ -182,28 +222,37 @@ namespace osu.Game.Beatmaps // If we have an ID then we already exist in the database. if (beatmapSetInfo.ID != 0) return; - beatmaps.Add(beatmapSetInfo); + createBeatmapStore(createContext).Add(beatmapSetInfo); } /// /// Downloads a beatmap. /// /// The to be downloaded. - /// A new , or an existing one if a download is already in progress. - public DownloadBeatmapSetRequest Download(BeatmapSetInfo beatmapSetInfo) + /// Whether the beatmap should be downloaded without video. Defaults to false. + public void Download(BeatmapSetInfo beatmapSetInfo, bool noVideo = false) { var existing = GetExistingDownload(beatmapSetInfo); - if (existing != null) return existing; + if (existing != null || api == null) return; - if (api == null) return null; - - ProgressNotification downloadNotification = new ProgressNotification + if (!api.LocalUser.Value.IsSupporter) { + PostNotification?.Invoke(new SimpleNotification + { + Icon = FontAwesome.fa_superpowers, + Text = "You gotta be a supporter to download for now 'yo" + }); + return; + } + + var downloadNotification = new ProgressNotification + { + CompletionText = $"Imported {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}!", Text = $"Downloading {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}", }; - var request = new DownloadBeatmapSetRequest(beatmapSetInfo); + var request = new DownloadBeatmapSetRequest(beatmapSetInfo, noVideo); request.DownloadProgressed += progress => { @@ -213,11 +262,17 @@ namespace osu.Game.Beatmaps request.Success += data => { - downloadNotification.State = ProgressNotificationState.Completed; + downloadNotification.Text = $"Importing {beatmapSetInfo.Metadata.Artist} - {beatmapSetInfo.Metadata.Title}"; - using (var stream = new MemoryStream(data)) - using (var archive = new OszArchiveReader(stream)) - Import(archive); + Task.Factory.StartNew(() => + { + // This gets scheduled back to the update thread, but we want the import to run in the background. + using (var stream = new MemoryStream(data)) + using (var archive = new OszArchiveReader(stream)) + Import(archive); + + downloadNotification.State = ProgressNotificationState.Completed; + }, TaskCreationOptions.LongRunning); currentDownloads.Remove(request); }; @@ -241,9 +296,8 @@ namespace osu.Game.Beatmaps PostNotification?.Invoke(downloadNotification); // don't run in the main api queue as this is a long-running task. - Task.Run(() => request.Perform(api)); - - return request; + Task.Factory.StartNew(() => request.Perform(api), TaskCreationOptions.LongRunning); + BeatmapDownloadBegan?.Invoke(request); } /// @@ -260,10 +314,86 @@ namespace osu.Game.Beatmaps /// The beatmap set to delete. public void Delete(BeatmapSetInfo beatmapSet) { - if (!beatmaps.Delete(beatmapSet)) return; + lock (importContext) + { + var context = importContext.Value; - if (!beatmapSet.Protected) - files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); + using (var transaction = context.BeginTransaction()) + { + context.ChangeTracker.AutoDetectChangesEnabled = false; + + // re-fetch the beatmap set on the import context. + beatmapSet = context.BeatmapSetInfo.Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == beatmapSet.ID); + + // create local stores so we can isolate and thread safely, and share a context/transaction. + var iFiles = new FileStore(() => context, storage); + var iBeatmaps = createBeatmapStore(() => context); + + if (iBeatmaps.Delete(beatmapSet)) + { + if (!beatmapSet.Protected) + iFiles.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); + } + + context.ChangeTracker.AutoDetectChangesEnabled = true; + context.SaveChanges(transaction); + } + } + } + + public void UndeleteAll() + { + var deleteMaps = QueryBeatmapSets(bs => bs.DeletePending).ToList(); + + if (!deleteMaps.Any()) return; + + var notification = new ProgressNotification + { + CompletionText = "Restored all deleted beatmaps!", + Progress = 0, + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + int i = 0; + + foreach (var bs in deleteMaps) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Restoring ({i} of {deleteMaps.Count})"; + notification.Progress = (float)++i / deleteMaps.Count; + Undelete(bs); + } + + notification.State = ProgressNotificationState.Completed; + } + + public void Undelete(BeatmapSetInfo beatmapSet) + { + if (beatmapSet.Protected) + return; + + lock (importContext) + { + var context = importContext.Value; + + using (var transaction = context.BeginTransaction()) + { + context.ChangeTracker.AutoDetectChangesEnabled = false; + + var iFiles = new FileStore(() => context, storage); + var iBeatmaps = createBeatmapStore(() => context); + + undelete(iBeatmaps, iFiles, beatmapSet); + + context.ChangeTracker.AutoDetectChangesEnabled = true; + context.SaveChanges(transaction); + } + } } /// @@ -282,8 +412,10 @@ namespace osu.Game.Beatmaps /// 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 store to restore beatmaps from. + /// The store to restore beatmap files from. /// The beatmap to restore. - public void Undelete(BeatmapSetInfo beatmapSet) + private void undelete(BeatmapStore beatmaps, FileStore files, BeatmapSetInfo beatmapSet) { if (!beatmaps.Undelete(beatmapSet)) return; @@ -299,15 +431,9 @@ namespace osu.Game.Beatmaps /// A instance correlating to the provided . public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap previous = null) { - if (beatmapInfo == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo) + if (beatmapInfo?.BeatmapSet == 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; @@ -318,32 +444,12 @@ namespace osu.Game.Beatmaps 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; - } - } + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().FirstOrDefault(query); /// /// Refresh an existing instance of a from the store. @@ -357,35 +463,21 @@ namespace osu.Game.Beatmaps /// /// The query. /// Results from the provided query. - public List QueryBeatmapSets(Expression> query) - { - return beatmaps.QueryAndPopulate(query); - } + public IEnumerable QueryBeatmapSets(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().Where(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) - { - BeatmapInfo set = beatmaps.Query().FirstOrDefault(query); - - if (set != null) - beatmaps.Populate(set); - - return set; - } + public BeatmapInfo QueryBeatmap(Expression> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query); /// /// 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); - } + public IEnumerable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); /// /// Creates an from a valid storage path. @@ -395,18 +487,20 @@ namespace osu.Game.Beatmaps private ArchiveReader getReaderFrom(string path) { if (ZipFile.IsZipFile(path)) + // ReSharper disable once InconsistentlySynchronizedField return new OszArchiveReader(storage.GetStream(path)); - else - return new LegacyFilesystemReader(path); + return new LegacyFilesystemReader(path); } /// /// Import a beamap into our local storage. /// If the beatmap is already imported, the existing instance will be returned. /// + /// The store to import beatmap files to. + /// The store to import beatmaps to. /// The beatmap archive to be read. /// The imported beatmap, or an existing instance if it is already present. - private BeatmapSetInfo importToStorage(ArchiveReader reader) + private BeatmapSetInfo importToStorage(FileStore files, BeatmapStore beatmaps, ArchiveReader reader) { // let's make sure there are actually .osu files to import. string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); @@ -422,13 +516,11 @@ namespace osu.Game.Beatmaps 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(); + var beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == hash); if (beatmapSet != null) { - Undelete(beatmapSet); + undelete(beatmaps, files, beatmapSet); // ensure all files are present and accessible foreach (var f in beatmapSet.Files) @@ -438,6 +530,8 @@ namespace osu.Game.Beatmaps files.Add(s, false); } + // todo: delete any files which shouldn't exist any more. + return beatmapSet; } @@ -455,16 +549,23 @@ namespace osu.Game.Beatmaps BeatmapMetadata metadata; using (var stream = new StreamReader(reader.GetStream(mapName))) - metadata = BeatmapDecoder.GetDecoder(stream).Decode(stream).Metadata; + metadata = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata; + + + // check if a set already exists with the same online id. + if (metadata.OnlineBeatmapSetID != null) + beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == metadata.OnlineBeatmapSetID); + + if (beatmapSet == null) + beatmapSet = new BeatmapSetInfo + { + OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, + Beatmaps = new List(), + Hash = hash, + Files = fileInfos, + Metadata = 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")); @@ -477,22 +578,29 @@ namespace osu.Game.Beatmaps raw.CopyTo(ms); ms.Position = 0; - var decoder = BeatmapDecoder.GetDecoder(sr); - Beatmap beatmap = decoder.Decode(sr); + var decoder = Decoder.GetDecoder(sr); + Beatmap beatmap = decoder.DecodeBeatmap(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; + var existing = beatmaps.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.BeatmapInfo.Hash || beatmap.BeatmapInfo.OnlineBeatmapID != null && b.OnlineBeatmapID == beatmap.BeatmapInfo.OnlineBeatmapID); - // 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; + if (existing == null) + { + // Exclude beatmap-metadata if it's equal to beatmapset-metadata + if (metadata.Equals(beatmap.Metadata)) + beatmap.BeatmapInfo.Metadata = null; - beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); + RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); + + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.BeatmapInfo.Ruleset = ruleset; + beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0; + + beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); + } } } @@ -502,17 +610,10 @@ namespace osu.Game.Beatmaps /// /// 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) + public List GetAllUsableBeatmapSets() { - lock (beatmaps) - { - if (populate) - return beatmaps.QueryAndPopulate(b => !b.DeletePending).ToList(); - else - return beatmaps.Query(b => !b.DeletePending).ToList(); - } + return beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); } protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap @@ -529,28 +630,19 @@ namespace osu.Game.Beatmaps { try { - Beatmap beatmap; - - BeatmapDecoder decoder; using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) { - decoder = BeatmapDecoder.GetDecoder(stream); - beatmap = decoder.Decode(stream); + Decoder decoder = Decoder.GetDecoder(stream); + return decoder.DecodeBeatmap(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; } + catch + { + return null; + } } - private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => f.Filename == filename).FileInfo.StoragePath; + private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; protected override Texture GetBackground() { @@ -561,7 +653,10 @@ namespace osu.Game.Beatmaps { return new TextureStore(new RawTextureLoaderStore(store), false).Get(getPathForFile(Metadata.BackgroundFile)); } - catch { return null; } + catch + { + return null; + } } protected override Track GetTrack() @@ -571,7 +666,34 @@ namespace osu.Game.Beatmaps var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); return trackData == null ? null : new TrackBass(trackData); } - catch { return new TrackVirtual(); } + catch + { + return new TrackVirtual(); + } + } + + protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile))); + + protected override Storyboard GetStoryboard() + { + if (BeatmapInfo?.Path == null && BeatmapSetInfo?.StoryboardFile == null) + return new Storyboard(); + + try + { + Decoder decoder; + using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo?.Path)))) + decoder = Decoder.GetDecoder(stream); + + // try for .osb first and fall back to .osu + string storyboardFile = BeatmapSetInfo.StoryboardFile ?? BeatmapInfo.Path; + using (var stream = new StreamReader(store.GetStream(getPathForFile(storyboardFile)))) + return decoder.GetStoryboardDecoder().DecodeStoryboard(stream); + } + catch + { + return new Storyboard(); + } } } @@ -593,13 +715,14 @@ namespace osu.Game.Beatmaps public void DeleteAll() { - var maps = GetAllUsableBeatmapSets().ToArray(); + var maps = GetAllUsableBeatmapSets(); - if (maps.Length == 0) return; + if (maps.Count == 0) return; var notification = new ProgressNotification { Progress = 0, + CompletionText = "Deleted all beatmaps!", State = ProgressNotificationState.Active, }; @@ -613,8 +736,8 @@ namespace osu.Game.Beatmaps // user requested abort return; - notification.Text = $"Deleting ({i} of {maps.Length})"; - notification.Progress = (float)++i / maps.Length; + notification.Text = $"Deleting ({i} of {maps.Count})"; + notification.Progress = (float)++i / maps.Count; Delete(b); } diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index cc9a51b4e2..9010f922bb 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -1,26 +1,59 @@ // 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.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; -using SQLite.Net.Attributes; +using osu.Game.Users; namespace osu.Game.Beatmaps { - public class BeatmapMetadata + [Serializable] + public class BeatmapMetadata : IEquatable { - [PrimaryKey, AutoIncrement] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [JsonIgnore] public int ID { get; set; } - public int? OnlineBeatmapSetID { get; set; } + private int? onlineBeatmapSetID; + + [NotMapped] + [JsonProperty(@"id")] + public int? OnlineBeatmapSetID + { + get { return onlineBeatmapSetID; } + set { onlineBeatmapSetID = value > 0 ? value : null; } + } public string Title { get; set; } public string TitleUnicode { get; set; } public string Artist { get; set; } public string ArtistUnicode { get; set; } + [JsonIgnore] + public List Beatmaps { get; set; } + + [JsonIgnore] + public List BeatmapSets { get; set; } + + /// + /// Helper property to deserialize a username to . + /// [JsonProperty(@"creator")] - public string Author { get; set; } + [Column("Author")] + public string AuthorString + { + get { return Author?.Username; } + set { Author = new User { Username = value }; } + } + + /// + /// The author of the beatmaps in this set. + /// + [JsonIgnore] + public User Author; public string Source { get; set; } @@ -30,9 +63,12 @@ namespace osu.Game.Beatmaps public string AudioFile { get; set; } public string BackgroundFile { get; set; } + public override string ToString() => $"{Artist} - {Title} ({Author})"; + + [JsonIgnore] public string[] SearchableTerms => new[] { - Author, + Author?.Username, Artist, ArtistUnicode, Title, @@ -40,5 +76,23 @@ namespace osu.Game.Beatmaps Source, Tags }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); + + public bool Equals(BeatmapMetadata other) + { + if (other == null) + return false; + + return onlineBeatmapSetID == other.onlineBeatmapSetID + && Title == other.Title + && TitleUnicode == other.TitleUnicode + && Artist == other.Artist + && ArtistUnicode == other.ArtistUnicode + && AuthorString == other.AuthorString + && Source == other.Source + && Tags == other.Tags + && PreviewTime == other.PreviewTime + && AudioFile == other.AudioFile + && BackgroundFile == other.BackgroundFile; + } } } diff --git a/osu.Game/Beatmaps/BeatmapMetrics.cs b/osu.Game/Beatmaps/BeatmapMetrics.cs index 730cf635da..e0cd5f10e7 100644 --- a/osu.Game/Beatmaps/BeatmapMetrics.cs +++ b/osu.Game/Beatmaps/BeatmapMetrics.cs @@ -12,7 +12,7 @@ namespace osu.Game.Beatmaps public class BeatmapMetrics { /// - /// Total vote counts of user ratings on a scale of 0..length. + /// Total vote counts of user ratings on a scale of 0..10 where 0 is unused (probably will be fixed at API?). /// public IEnumerable Ratings { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapOnlineInfo.cs index 399cabda99..6a988036c5 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineInfo.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineInfo.cs @@ -1,8 +1,6 @@ // 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 { /// @@ -15,33 +13,24 @@ namespace osu.Game.Beatmaps /// public double Length { get; set; } - /// - /// Whether or not this beatmap has a background video. - /// - public bool HasVideo { get; set; } - /// /// The amount of circles in this beatmap. /// - [JsonProperty(@"count_circles")] public int CircleCount { get; set; } /// /// The amount of sliders in this beatmap. /// - [JsonProperty(@"count_sliders")] public int SliderCount { get; set; } /// /// 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 index a05362b32d..0e3aa61d9f 100644 --- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -1,27 +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.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using osu.Game.IO; -using SQLite.Net.Attributes; -using SQLiteNetExtensions.Attributes; namespace osu.Game.Beatmaps { public class BeatmapSetFileInfo { - [PrimaryKey, AutoIncrement] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 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] + [Required] public string Filename { get; set; } } -} \ No newline at end of file +} diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index f47affcab8..a41beaab81 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -2,43 +2,39 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; -using SQLite.Net.Attributes; -using SQLiteNetExtensions.Attributes; +using osu.Game.Database; namespace osu.Game.Beatmaps { - public class BeatmapSetInfo + public class BeatmapSetInfo : IHasPrimaryKey { - [PrimaryKey, AutoIncrement] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } public int? OnlineBeatmapSetID { get; set; } - [OneToOne(CascadeOperations = CascadeOperation.All)] public BeatmapMetadata Metadata { get; set; } - [NotNull, ForeignKey(typeof(BeatmapMetadata))] - public int BeatmapMetadataID { get; set; } - - [OneToMany(CascadeOperations = CascadeOperation.All)] public List Beatmaps { get; set; } - [Ignore] + [NotMapped] public BeatmapSetOnlineInfo OnlineInfo { get; set; } public double MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty); - [Indexed] + [NotMapped] public bool DeletePending { get; set; } public string Hash { get; set; } public string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(".osb"))?.Filename; - [OneToMany(CascadeOperations = CascadeOperation.All)] public List Files { get; set; } + public override string ToString() => Metadata.ToString(); + public bool Protected { get; set; } } } diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs index 6b59f0f298..b38f74b3f7 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs @@ -3,7 +3,6 @@ using System; using Newtonsoft.Json; -using osu.Game.Users; namespace osu.Game.Beatmaps { @@ -12,11 +11,6 @@ namespace osu.Game.Beatmaps /// public class BeatmapSetOnlineInfo { - /// - /// The author of the beatmaps in this set. - /// - public User Author; - /// /// The date this beatmap set was submitted to the online listing. /// @@ -32,16 +26,19 @@ namespace osu.Game.Beatmaps /// public DateTimeOffset? LastUpdated { get; set; } + /// + /// Whether or not this beatmap set has a background video. + /// + public bool HasVideo { get; set; } + /// /// 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; } /// @@ -52,13 +49,11 @@ namespace osu.Game.Beatmaps /// /// 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; } } diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 0f2d8cffa6..fb45c17454 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -2,9 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; using osu.Game.Database; -using SQLite.Net; -using SQLiteNetExtensions.Extensions; namespace osu.Game.Beatmaps { @@ -19,89 +19,33 @@ namespace osu.Game.Beatmaps public event Action BeatmapHidden; public event Action BeatmapRestored; - /// - /// The current version of this store. Used for migrations (see ). - /// The initial version is 1. - /// - protected override int StoreVersion => 4; - - public BeatmapStore(SQLiteConnection connection) - : base(connection) + public BeatmapStore(Func factory) + : base(factory) { } - 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; - case 4: - // Added Hidden column to BeatmapInfo - Connection.MigrateTable(); - break; - } - } - } - /// /// Add a to the database. /// /// The beatmap to add. public void Add(BeatmapSetInfo beatmapSet) { - Connection.RunInTransaction(() => + var context = GetContext(); + + foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null)) { - Connection.InsertOrReplaceWithChildren(beatmapSet, true); - }); + // If we detect a new metadata object it'll be attached to the current context so it can be reused + // to prevent duplicate entries when persisting. To accomplish this we look in the cache (.Local) + // of the corresponding table (.Set()) for matching entries to our criteria. + var contextMetadata = context.Set().Local.SingleOrDefault(e => e.Equals(beatmap.Metadata)); + if (contextMetadata != null) + beatmap.Metadata = contextMetadata; + else + context.BeatmapMetadata.Attach(beatmap.Metadata); + } + + context.BeatmapSetInfo.Attach(beatmapSet); + context.SaveChanges(); BeatmapSetAdded?.Invoke(beatmapSet); } @@ -113,10 +57,13 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Delete(BeatmapSetInfo beatmapSet) { - if (beatmapSet.DeletePending) return false; + var context = GetContext(); + Refresh(ref beatmapSet, BeatmapSets); + + if (beatmapSet.DeletePending) return false; beatmapSet.DeletePending = true; - Connection.Update(beatmapSet); + context.SaveChanges(); BeatmapSetRemoved?.Invoke(beatmapSet); return true; @@ -129,10 +76,13 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Undelete(BeatmapSetInfo beatmapSet) { - if (!beatmapSet.DeletePending) return false; + var context = GetContext(); + Refresh(ref beatmapSet, BeatmapSets); + + if (!beatmapSet.DeletePending) return false; beatmapSet.DeletePending = false; - Connection.Update(beatmapSet); + context.SaveChanges(); BeatmapSetAdded?.Invoke(beatmapSet); return true; @@ -145,10 +95,13 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Hide(BeatmapInfo beatmap) { - if (beatmap.Hidden) return false; + var context = GetContext(); + Refresh(ref beatmap, Beatmaps); + + if (beatmap.Hidden) return false; beatmap.Hidden = true; - Connection.Update(beatmap); + context.SaveChanges(); BeatmapHidden?.Invoke(beatmap); return true; @@ -161,22 +114,50 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Restore(BeatmapInfo beatmap) { - if (!beatmap.Hidden) return false; + var context = GetContext(); + Refresh(ref beatmap, Beatmaps); + + if (!beatmap.Hidden) return false; beatmap.Hidden = false; - Connection.Update(beatmap); + context.SaveChanges(); BeatmapRestored?.Invoke(beatmap); return true; } - private void cleanupPendingDeletions() + public override void Cleanup() { - Connection.RunInTransaction(() => - { - foreach (var b in QueryAndPopulate(b => b.DeletePending && !b.Protected)) - Connection.Delete(b, true); - }); + var context = GetContext(); + + var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Metadata); + + // metadata is M-N so we can't rely on cascades + context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata)); + context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); + + // todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly. + context.BeatmapDifficulty.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty))); + + // cascades down to beatmaps. + context.BeatmapSetInfo.RemoveRange(purgeable); + context.SaveChanges(); } + + public IQueryable BeatmapSets => GetContext().BeatmapSetInfo + .Include(s => s.Metadata) + .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Files).ThenInclude(f => f.FileInfo); + + public IQueryable Beatmaps => GetContext().BeatmapInfo + .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata) + .Include(b => b.Metadata) + .Include(b => b.Ruleset) + .Include(b => b.BaseDifficulty); } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index e7035880dd..f031ebe353 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -1,33 +1,40 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; using osu.Framework.Lists; namespace osu.Game.Beatmaps.ControlPoints { + [Serializable] public class ControlPointInfo { /// /// All timing points. /// - public readonly SortedList TimingPoints = new SortedList(Comparer.Default); + [JsonProperty] + public SortedList TimingPoints { get; private set; } = new SortedList(Comparer.Default); /// /// All difficulty points. /// - public readonly SortedList DifficultyPoints = new SortedList(Comparer.Default); + [JsonProperty] + public SortedList DifficultyPoints { get; private set; } = new SortedList(Comparer.Default); /// /// All sound points. /// - public readonly SortedList SoundPoints = new SortedList(Comparer.Default); + [JsonProperty] + public SortedList SamplePoints { get; private set; } = new SortedList(Comparer.Default); /// /// All effect points. /// - public readonly SortedList EffectPoints = new SortedList(Comparer.Default); + [JsonProperty] + public SortedList EffectPoints { get; private set; } = new SortedList(Comparer.Default); /// /// Finds the difficulty control point that is active at . @@ -48,7 +55,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time to find the sound control point at. /// The sound control point. - public SoundControlPoint SoundPointAt(double time) => binarySearch(SoundPoints, time); + public SampleControlPoint SamplePointAt(double time) => binarySearch(SamplePoints, time, SamplePoints.FirstOrDefault()); /// /// Finds the timing control point that is active at . @@ -57,18 +64,21 @@ namespace osu.Game.Beatmaps.ControlPoints /// The timing control point. public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.FirstOrDefault()); + [JsonIgnore] /// /// Finds the maximum BPM represented by any timing control point. /// public double BPMMaximum => 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; + [JsonIgnore] /// /// Finds the minimum BPM represented by any timing control point. /// public double BPMMinimum => 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; + [JsonIgnore] /// /// Finds the mode BPM (most common BPM) represented by the control points. /// @@ -85,6 +95,9 @@ namespace osu.Game.Beatmaps.ControlPoints private T binarySearch(SortedList list, double time, T prePoint = null) where T : ControlPoint, new() { + if (list == null) + throw new ArgumentNullException(nameof(list)); + if (list.Count == 0) return new T(); @@ -104,4 +117,4 @@ namespace osu.Game.Beatmaps.ControlPoints return list[index - 1]; } } -} \ No newline at end of file +} diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs new file mode 100644 index 0000000000..40e45da13c --- /dev/null +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Audio; + +namespace osu.Game.Beatmaps.ControlPoints +{ + public class SampleControlPoint : ControlPoint + { + public const string DEFAULT_BANK = "normal"; + + /// + /// The default sample bank at this control point. + /// + public string SampleBank = DEFAULT_BANK; + + /// + /// The default sample volume at this control point. + /// + public int SampleVolume; + + /// + /// Create a SampleInfo based on the sample settings in this control point. + /// + /// The name of the same. + /// A populated . + public SampleInfo GetSampleInfo(string sampleName = SampleInfo.HIT_NORMAL) => new SampleInfo + { + Bank = SampleBank, + Name = sampleName, + Volume = SampleVolume, + }; + } +} diff --git a/osu.Game/Beatmaps/ControlPoints/SoundControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SoundControlPoint.cs deleted file mode 100644 index 8084229382..0000000000 --- a/osu.Game/Beatmaps/ControlPoints/SoundControlPoint.cs +++ /dev/null @@ -1,18 +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.Beatmaps.ControlPoints -{ - public class SoundControlPoint : ControlPoint - { - /// - /// The default sample bank at this control point. - /// - public string SampleBank; - - /// - /// The default sample volume at this control point. - /// - public int SampleVolume; - } -} \ No newline at end of file diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs index 60cbf0ac61..687e1b2177 100644 --- a/osu.Game/Beatmaps/DifficultyCalculator.cs +++ b/osu.Game/Beatmaps/DifficultyCalculator.cs @@ -3,6 +3,10 @@ using osu.Game.Rulesets.Objects; using System.Collections.Generic; +using osu.Game.Rulesets.Mods; +using osu.Framework.Timing; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; namespace osu.Game.Beatmaps { @@ -10,45 +14,46 @@ namespace osu.Game.Beatmaps { protected double TimeRate = 1; - protected abstract double CalculateInternal(Dictionary categoryDifficulty); - - private void loadTiming() - { - // TODO: Handle mods - const int audio_rate = 100; - TimeRate = audio_rate / 100.0; - } - - public double Calculate(Dictionary categoryDifficulty = null) - { - loadTiming(); - double difficulty = CalculateInternal(categoryDifficulty); - return difficulty; - } + public abstract double Calculate(Dictionary categoryDifficulty = null); } public abstract class DifficultyCalculator : DifficultyCalculator where T : HitObject { - protected readonly Beatmap Beatmap; + protected readonly Beatmap Beatmap; + protected readonly Mod[] Mods; - protected List Objects; - - protected DifficultyCalculator(Beatmap beatmap) + protected DifficultyCalculator(Beatmap beatmap, Mod[] mods = null) { - Beatmap = beatmap; + Beatmap = CreateBeatmapConverter(beatmap).Convert(beatmap); + Mods = mods ?? new Mod[0]; - Objects = CreateBeatmapConverter().Convert(beatmap).HitObjects; - foreach (var h in Objects) - h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.Difficulty); + ApplyMods(Mods); PreprocessHitObjects(); } + protected virtual void ApplyMods(Mod[] mods) + { + var clock = new StopwatchClock(); + mods.OfType().ForEach(m => m.ApplyToClock(clock)); + TimeRate = clock.Rate; + + foreach (var mod in Mods.OfType()) + mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty); + + foreach (var mod in mods.OfType>()) + foreach (var obj in Beatmap.HitObjects) + mod.ApplyToHitObject(obj); + + foreach (var h in Beatmap.HitObjects) + h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty); + } + protected virtual void PreprocessHitObjects() { } - protected abstract BeatmapConverter CreateBeatmapConverter(); + protected abstract BeatmapConverter CreateBeatmapConverter(Beatmap beatmap); } } diff --git a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs index 9b897b4912..e4904786c7 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs @@ -1,17 +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.Allocation; using osu.Framework.Graphics.Sprites; namespace osu.Game.Beatmaps.Drawables { - internal class BeatmapBackgroundSprite : Sprite + public class BeatmapBackgroundSprite : Sprite { private readonly WorkingBeatmap working; public BeatmapBackgroundSprite(WorkingBeatmap working) { + if (working == null) + throw new ArgumentNullException(nameof(working)); + this.working = working; } diff --git a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs b/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs deleted file mode 100644 index 9c62289bfa..0000000000 --- a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs +++ /dev/null @@ -1,131 +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; -using osu.Framework.Graphics; - -namespace osu.Game.Beatmaps.Drawables -{ - public class BeatmapGroup : IStateful - { - public event Action StateChanged; - - public BeatmapPanel SelectedPanel; - - /// - /// Fires when one of our difficulties was selected. Will fire on first expand. - /// - public Action SelectionChanged; - - /// - /// Fires when one of our difficulties is clicked when already selected. Should start playing the map. - /// - public Action StartRequested; - - public Action DeleteRequested; - - public Action RestoreHiddenRequested; - - public Action HideDifficultyRequested; - - public BeatmapSetHeader Header; - - public List BeatmapPanels; - - public BeatmapSetInfo BeatmapSet; - - private BeatmapGroupState state; - public BeatmapGroupState State - { - get { return state; } - set - { - state = value; - - switch (value) - { - case BeatmapGroupState.Expanded: - Header.State = PanelSelectedState.Selected; - foreach (BeatmapPanel panel in BeatmapPanels) - panel.State = panel == SelectedPanel ? PanelSelectedState.Selected : PanelSelectedState.NotSelected; - break; - case BeatmapGroupState.Collapsed: - Header.State = PanelSelectedState.NotSelected; - foreach (BeatmapPanel panel in BeatmapPanels) - panel.State = PanelSelectedState.Hidden; - break; - case BeatmapGroupState.Hidden: - Header.State = PanelSelectedState.Hidden; - foreach (BeatmapPanel panel in BeatmapPanels) - panel.State = PanelSelectedState.Hidden; - break; - } - - StateChanged?.Invoke(state); - } - } - - public BeatmapGroup(BeatmapSetInfo beatmapSet, BeatmapManager manager) - { - BeatmapSet = beatmapSet; - WorkingBeatmap beatmap = manager.GetWorkingBeatmap(BeatmapSet.Beatmaps.FirstOrDefault()); - - Header = new BeatmapSetHeader(beatmap) - { - GainedSelection = headerGainedSelection, - DeleteRequested = b => DeleteRequested(b), - RestoreHiddenRequested = b => RestoreHiddenRequested(b), - RelativeSizeAxes = Axes.X, - }; - - BeatmapSet.Beatmaps = BeatmapSet.Beatmaps.Where(b => !b.Hidden).OrderBy(b => b.StarDifficulty).ToList(); - BeatmapPanels = BeatmapSet.Beatmaps.Select(b => new BeatmapPanel(b) - { - Alpha = 0, - GainedSelection = panelGainedSelection, - HideRequested = p => HideDifficultyRequested?.Invoke(p), - StartRequested = p => { StartRequested?.Invoke(p.Beatmap); }, - RelativeSizeAxes = Axes.X, - }).ToList(); - - Header.AddDifficultyIcons(BeatmapPanels); - } - - - private void headerGainedSelection(BeatmapSetHeader panel) - { - State = BeatmapGroupState.Expanded; - - //we want to make sure one of our children is selected in the case none have been selected yet. - if (SelectedPanel == null) - BeatmapPanels.First().State = PanelSelectedState.Selected; - } - - private void panelGainedSelection(BeatmapPanel panel) - { - try - { - if (SelectedPanel == panel) return; - - if (SelectedPanel != null) - SelectedPanel.State = PanelSelectedState.NotSelected; - SelectedPanel = panel; - } - finally - { - State = BeatmapGroupState.Expanded; - SelectionChanged?.Invoke(this, SelectedPanel); - } - } - } - - public enum BeatmapGroupState - { - Collapsed, - Expanded, - Hidden, - } -} diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs index df7e0905d0..ba79db3f48 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetCover.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.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -10,18 +11,44 @@ namespace osu.Game.Beatmaps.Drawables public class BeatmapSetCover : Sprite { private readonly BeatmapSetInfo set; - public BeatmapSetCover(BeatmapSetInfo set) + private readonly BeatmapSetCoverType type; + + public BeatmapSetCover(BeatmapSetInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover) { + if (set == null) + throw new ArgumentNullException(nameof(set)); + this.set = set; + this.type = type; } [BackgroundDependencyLoader] private void load(TextureStore textures) { - string resource = set.OnlineInfo.Covers.Cover; + string resource = null; + + switch (type) + { + case BeatmapSetCoverType.Cover: + resource = set.OnlineInfo.Covers.Cover; + break; + case BeatmapSetCoverType.Card: + resource = set.OnlineInfo.Covers.Card; + break; + case BeatmapSetCoverType.List: + resource = set.OnlineInfo.Covers.List; + break; + } if (resource != null) Texture = textures.Get(resource); } } + + public enum BeatmapSetCoverType + { + Cover, + Card, + List, + } } diff --git a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs b/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs index 41b77f6584..57a5abc4c7 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.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.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -23,6 +24,9 @@ namespace osu.Game.Beatmaps.Drawables [BackgroundDependencyLoader] private void load(OsuColour palette) { + if (palette == null) + throw new ArgumentNullException(nameof(palette)); + this.palette = palette; AccentColour = getColour(beatmap); } @@ -39,6 +43,9 @@ namespace osu.Game.Beatmaps.Drawables private DifficultyRating getDifficultyRating(BeatmapInfo beatmap) { + if (beatmap == null) + throw new ArgumentNullException(nameof(beatmap)); + var rating = beatmap.StarDifficulty; if (rating < 1.5) return DifficultyRating.Easy; diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 42db025a40..8259da9492 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.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.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; @@ -15,6 +16,9 @@ namespace osu.Game.Beatmaps.Drawables public DifficultyIcon(BeatmapInfo beatmap) : base(beatmap) { + if (beatmap == null) + throw new ArgumentNullException(nameof(beatmap)); + this.beatmap = beatmap; Size = new Vector2(20); } @@ -35,7 +39,8 @@ namespace osu.Game.Beatmaps.Drawables new ConstrainedIconContainer { RelativeSizeAxes = Axes.Both, - Icon = beatmap.Ruleset.CreateInstance().CreateIcon() + // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) + Icon = beatmap.Ruleset?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.fa_question_circle_o } } }; } diff --git a/osu.Game/Beatmaps/Drawables/Panel.cs b/osu.Game/Beatmaps/Drawables/Panel.cs deleted file mode 100644 index d6ed306b39..0000000000 --- a/osu.Game/Beatmaps/Drawables/Panel.cs +++ /dev/null @@ -1,132 +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; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; - -namespace osu.Game.Beatmaps.Drawables -{ - public class Panel : Container, IStateful - { - public const float MAX_HEIGHT = 80; - - public event Action StateChanged; - - public override bool RemoveWhenNotAlive => false; - - private readonly Container nestedContainer; - - protected override Container Content => nestedContainer; - - protected Panel() - { - Height = MAX_HEIGHT; - RelativeSizeAxes = Axes.X; - - AddInternal(nestedContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - BorderColour = new Color4(221, 255, 255, 255), - }); - - Alpha = 0; - } - - public void SetMultiplicativeAlpha(float alpha) - { - nestedContainer.Alpha = alpha; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - ApplyState(); - } - - protected virtual void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden) - { - if (!IsLoaded) return; - - switch (state) - { - case PanelSelectedState.Hidden: - case PanelSelectedState.NotSelected: - Deselected(); - break; - case PanelSelectedState.Selected: - Selected(); - break; - } - - if (state == PanelSelectedState.Hidden) - this.FadeOut(300, Easing.OutQuint); - else - this.FadeIn(250); - } - - private PanelSelectedState state = PanelSelectedState.NotSelected; - - public PanelSelectedState State - { - get { return state; } - - set - { - if (state == value) - return; - - var last = state; - state = value; - - ApplyState(last); - - StateChanged?.Invoke(State); - } - } - - protected virtual void Selected() - { - nestedContainer.BorderThickness = 2.5f; - nestedContainer.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = new Color4(130, 204, 255, 150), - Radius = 20, - Roundness = 10, - }; - } - - protected virtual void Deselected() - { - nestedContainer.BorderThickness = 0; - nestedContainer.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(1), - Radius = 10, - Colour = Color4.Black.Opacity(100), - }; - } - - protected override bool OnClick(InputState state) - { - State = PanelSelectedState.Selected; - return true; - } - } - - public enum PanelSelectedState - { - Hidden, - NotSelected, - Selected - } -} diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index d8cd58d939..1434943da0 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Beatmaps { - internal class DummyWorkingBeatmap : WorkingBeatmap + public class DummyWorkingBeatmap : WorkingBeatmap { private readonly OsuGameBase game; @@ -22,10 +22,10 @@ namespace osu.Game.Beatmaps { Artist = "please load a beatmap!", Title = "no beatmaps available!", - Author = "no one", + AuthorString = "no one", }, BeatmapSet = new BeatmapSetInfo(), - Difficulty = new BeatmapDifficulty + BaseDifficulty = new BeatmapDifficulty { DrainRate = 0, CircleSize = 0, @@ -59,10 +59,12 @@ namespace osu.Game.Beatmaps throw new NotImplementedException(); } - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => null; + public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => null; public override string Description => "dummy"; + public override string ShortName => "dummy"; + public DummyRuleset(RulesetInfo rulesetInfo) : base(rulesetInfo) { diff --git a/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs deleted file mode 100644 index 81695c3b5a..0000000000 --- a/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs +++ /dev/null @@ -1,62 +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; - -namespace osu.Game.Beatmaps.Formats -{ - public abstract class BeatmapDecoder - { - private static readonly Dictionary decoders = new Dictionary(); - - static BeatmapDecoder() - { - OsuLegacyDecoder.Register(); - } - - public static BeatmapDecoder GetDecoder(StreamReader stream) - { - 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"); - return (BeatmapDecoder)Activator.CreateInstance(decoders[line], line); - } - - protected static void AddDecoder(string magic) where T : BeatmapDecoder - { - decoders[magic] = typeof(T); - } - - public virtual Beatmap Decode(StreamReader stream) - { - return ParseFile(stream); - } - - public virtual void Decode(StreamReader stream, Beatmap beatmap) - { - ParseFile(stream, beatmap); - } - - protected virtual Beatmap ParseFile(StreamReader stream) - { - var beatmap = new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - Metadata = new BeatmapMetadata(), - Difficulty = new BeatmapDifficulty(), - }, - }; - - ParseFile(stream, beatmap); - return beatmap; - } - - protected abstract void ParseFile(StreamReader stream, Beatmap beatmap); - } -} diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs new file mode 100644 index 0000000000..a6e2699262 --- /dev/null +++ b/osu.Game/Beatmaps/Formats/Decoder.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 System; +using System.Collections.Generic; +using System.IO; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps.Formats +{ + public abstract class Decoder + { + private static readonly Dictionary> decoders = new Dictionary>(); + + static Decoder() + { + LegacyDecoder.Register(); + JsonBeatmapDecoder.Register(); + } + + /// + /// Retrieves a to parse a . + /// + /// A stream pointing to the . + public static Decoder GetDecoder(StreamReader stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + 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"); + + return decoders[line](line); + } + + /// + /// Registers an instantiation function for a . + /// + /// A string in the file which triggers this decoder to be used. + /// A function which constructs the given . + protected static void AddDecoder(string magic, Func constructor) + { + decoders[magic] = constructor; + } + + /// + /// Retrieves a to parse a + /// + public abstract Decoder GetStoryboardDecoder(); + + public virtual Beatmap DecodeBeatmap(StreamReader stream) + { + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata(), + BaseDifficulty = new BeatmapDifficulty(), + }, + }; + + ParseBeatmap(stream, beatmap); + return beatmap; + } + + protected abstract void ParseBeatmap(StreamReader stream, Beatmap beatmap); + + public virtual Storyboard DecodeStoryboard(StreamReader stream) + { + var storyboard = new Storyboard(); + ParseStoryboard(stream, storyboard); + return storyboard; + } + + protected abstract void ParseStoryboard(StreamReader stream, Storyboard storyboard); + } +} diff --git a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs new file mode 100644 index 0000000000..29e7cee336 --- /dev/null +++ b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.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 System.IO; +using osu.Game.IO.Serialization; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps.Formats +{ + public class JsonBeatmapDecoder : Decoder + { + public static void Register() + { + AddDecoder("{", m => new JsonBeatmapDecoder()); + } + + public override Decoder GetStoryboardDecoder() => this; + + protected override void ParseBeatmap(StreamReader stream, Beatmap beatmap) + { + stream.BaseStream.Position = 0; + stream.DiscardBufferedData(); + + stream.ReadToEnd().DeserializeInto(beatmap); + + foreach (var hitObject in beatmap.HitObjects) + hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); + } + + protected override void ParseStoryboard(StreamReader stream, Storyboard storyboard) + { + // throw new System.NotImplementedException(); + } + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs new file mode 100644 index 0000000000..ea29e480ec --- /dev/null +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -0,0 +1,421 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Globalization; +using System.IO; +using OpenTK.Graphics; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Beatmaps.ControlPoints; +using System.Collections.Generic; + +namespace osu.Game.Beatmaps.Formats +{ + public class LegacyBeatmapDecoder : LegacyDecoder + { + private Beatmap beatmap; + + private bool hasCustomColours; + private ConvertHitObjectParser parser; + + private LegacySampleBank defaultSampleBank; + private int defaultSampleVolume = 100; + + public LegacyBeatmapDecoder() + { + } + + public LegacyBeatmapDecoder(string header) + { + BeatmapVersion = int.Parse(header.Substring(17)); + } + + protected override void ParseBeatmap(StreamReader stream, Beatmap beatmap) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + if (beatmap == null) + throw new ArgumentNullException(nameof(beatmap)); + + this.beatmap = beatmap; + this.beatmap.BeatmapInfo.BeatmapVersion = BeatmapVersion; + + ParseContent(stream); + + foreach (var hitObject in this.beatmap.HitObjects) + hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty); + } + + protected override bool ShouldSkipLine(string line) + { + if (base.ShouldSkipLine(line) || line.StartsWith(" ") || line.StartsWith("_")) + return true; + return false; + } + + protected override void ProcessSection(Section section, string line) + { + switch (section) + { + case Section.General: + handleGeneral(line); + break; + case Section.Editor: + handleEditor(line); + break; + case Section.Metadata: + handleMetadata(line); + break; + case Section.Difficulty: + handleDifficulty(line); + break; + case Section.Events: + handleEvents(line); + break; + case Section.TimingPoints: + handleTimingPoints(line); + break; + case Section.Colours: + handleColours(line); + break; + case Section.HitObjects: + handleHitObjects(line); + break; + case Section.Variables: + handleVariables(line); + break; + } + } + + private void handleGeneral(string line) + { + var pair = splitKeyVal(line, ':'); + + var metadata = beatmap.BeatmapInfo.Metadata; + switch (pair.Key) + { + case @"AudioFilename": + metadata.AudioFile = pair.Value; + break; + case @"AudioLeadIn": + beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value); + break; + case @"PreviewTime": + metadata.PreviewTime = int.Parse(pair.Value); + break; + case @"Countdown": + beatmap.BeatmapInfo.Countdown = int.Parse(pair.Value) == 1; + break; + case @"SampleSet": + defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value); + break; + case @"SampleVolume": + defaultSampleVolume = int.Parse(pair.Value); + break; + case @"StackLeniency": + beatmap.BeatmapInfo.StackLeniency = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"Mode": + beatmap.BeatmapInfo.RulesetID = int.Parse(pair.Value); + + switch (beatmap.BeatmapInfo.RulesetID) + { + case 0: + parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); + break; + case 1: + parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(); + break; + case 2: + parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(); + break; + case 3: + parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(); + break; + } + break; + case @"LetterboxInBreaks": + beatmap.BeatmapInfo.LetterboxInBreaks = int.Parse(pair.Value) == 1; + break; + case @"SpecialStyle": + beatmap.BeatmapInfo.SpecialStyle = int.Parse(pair.Value) == 1; + break; + case @"WidescreenStoryboard": + beatmap.BeatmapInfo.WidescreenStoryboard = int.Parse(pair.Value) == 1; + break; + } + } + + private void handleEditor(string line) + { + var pair = splitKeyVal(line, ':'); + + switch (pair.Key) + { + case @"Bookmarks": + beatmap.BeatmapInfo.StoredBookmarks = pair.Value; + break; + case @"DistanceSpacing": + beatmap.BeatmapInfo.DistanceSpacing = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"BeatDivisor": + beatmap.BeatmapInfo.BeatDivisor = int.Parse(pair.Value); + break; + case @"GridSize": + beatmap.BeatmapInfo.GridSize = int.Parse(pair.Value); + break; + case @"TimelineZoom": + beatmap.BeatmapInfo.TimelineZoom = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + } + } + + private void handleMetadata(string line) + { + var pair = splitKeyVal(line, ':'); + + var metadata = beatmap.BeatmapInfo.Metadata; + switch (pair.Key) + { + case @"Title": + metadata.Title = pair.Value; + break; + case @"TitleUnicode": + metadata.TitleUnicode = pair.Value; + break; + case @"Artist": + metadata.Artist = pair.Value; + break; + case @"ArtistUnicode": + metadata.ArtistUnicode = pair.Value; + break; + case @"Creator": + metadata.AuthorString = pair.Value; + break; + case @"Version": + beatmap.BeatmapInfo.Version = pair.Value; + break; + case @"Source": + beatmap.BeatmapInfo.Metadata.Source = pair.Value; + break; + case @"Tags": + beatmap.BeatmapInfo.Metadata.Tags = pair.Value; + break; + case @"BeatmapID": + beatmap.BeatmapInfo.OnlineBeatmapID = int.Parse(pair.Value); + break; + case @"BeatmapSetID": + beatmap.BeatmapInfo.OnlineBeatmapSetID = int.Parse(pair.Value); + metadata.OnlineBeatmapSetID = int.Parse(pair.Value); + break; + } + } + + private void handleDifficulty(string line) + { + var pair = splitKeyVal(line, ':'); + + var difficulty = beatmap.BeatmapInfo.BaseDifficulty; + switch (pair.Key) + { + case @"HPDrainRate": + difficulty.DrainRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"CircleSize": + difficulty.CircleSize = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"OverallDifficulty": + difficulty.OverallDifficulty = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"ApproachRate": + difficulty.ApproachRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"SliderMultiplier": + difficulty.SliderMultiplier = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"SliderTickRate": + difficulty.SliderTickRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + } + } + + private void handleEvents(string line) + { + DecodeVariables(ref line); + + string[] split = line.Split(','); + + EventType type; + if (!Enum.TryParse(split[0], out type)) + throw new InvalidDataException($@"Unknown event type {split[0]}"); + + switch (type) + { + case EventType.Background: + string filename = split[2].Trim('"'); + beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; + break; + case EventType.Break: + var breakEvent = new BreakPeriod + { + StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo), + EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo) + }; + + if (!breakEvent.HasEffect) + return; + + beatmap.Breaks.Add(breakEvent); + break; + } + } + + private void handleTimingPoints(string line) + { + string[] split = line.Split(','); + + double time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo); + double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); + double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; + + TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; + if (split.Length >= 3) + timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)int.Parse(split[2]); + + LegacySampleBank sampleSet = defaultSampleBank; + if (split.Length >= 4) + sampleSet = (LegacySampleBank)int.Parse(split[3]); + + //SampleBank sampleBank = SampleBank.Default; + //if (split.Length >= 5) + // sampleBank = (SampleBank)int.Parse(split[4]); + + int sampleVolume = defaultSampleVolume; + if (split.Length >= 6) + sampleVolume = int.Parse(split[5]); + + bool timingChange = true; + if (split.Length >= 7) + timingChange = split[6][0] == '1'; + + bool kiaiMode = false; + bool omitFirstBarSignature = false; + if (split.Length >= 8) + { + int effectFlags = int.Parse(split[7]); + kiaiMode = (effectFlags & 1) > 0; + omitFirstBarSignature = (effectFlags & 8) > 0; + } + + string stringSampleSet = sampleSet.ToString().ToLower(); + if (stringSampleSet == @"none") + stringSampleSet = @"normal"; + + DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(time); + SampleControlPoint samplePoint = beatmap.ControlPointInfo.SamplePointAt(time); + EffectControlPoint effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); + + if (timingChange) + { + beatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint + { + Time = time, + BeatLength = beatLength, + TimeSignature = timeSignature + }); + } + + if (speedMultiplier != difficultyPoint.SpeedMultiplier) + { + beatmap.ControlPointInfo.DifficultyPoints.RemoveAll(x => x.Time == time); + beatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint + { + Time = time, + SpeedMultiplier = speedMultiplier + }); + } + + if (stringSampleSet != samplePoint.SampleBank || sampleVolume != samplePoint.SampleVolume) + { + beatmap.ControlPointInfo.SamplePoints.Add(new SampleControlPoint + { + Time = time, + SampleBank = stringSampleSet, + SampleVolume = sampleVolume + }); + } + + if (kiaiMode != effectPoint.KiaiMode || omitFirstBarSignature != effectPoint.OmitFirstBarLine) + { + beatmap.ControlPointInfo.EffectPoints.Add(new EffectControlPoint + { + Time = time, + KiaiMode = kiaiMode, + OmitFirstBarLine = omitFirstBarSignature + }); + } + } + + private void handleColours(string line) + { + var pair = splitKeyVal(line, ':'); + + string[] split = pair.Value.Split(','); + + if (split.Length != 3) + throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B): {pair.Value}"); + + byte r, g, b; + if (!byte.TryParse(split[0], out r) || !byte.TryParse(split[1], out g) || !byte.TryParse(split[2], out b)) + throw new InvalidOperationException(@"Color must be specified with 8-bit integer components"); + + if (!hasCustomColours) + { + beatmap.ComboColors.Clear(); + hasCustomColours = true; + } + + // Note: the combo index specified in the beatmap is discarded + if (pair.Key.StartsWith(@"Combo")) + { + beatmap.ComboColors.Add(new Color4 + { + R = r / 255f, + G = g / 255f, + B = b / 255f, + A = 1f, + }); + } + } + + private void handleHitObjects(string line) + { + // 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) + beatmap.HitObjects.Add(obj); + } + + private void handleVariables(string line) + { + var pair = splitKeyVal(line, '='); + Variables[pair.Key] = pair.Value; + } + + private KeyValuePair splitKeyVal(string line, char separator) + { + var split = line.Trim().Split(new[] { separator }, 2); + + return new KeyValuePair + ( + split[0].Trim(), + split.Length > 1 ? split[1].Trim() : string.Empty + ); + } + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs new file mode 100644 index 0000000000..e5ced5f772 --- /dev/null +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -0,0 +1,163 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps.Formats +{ + public abstract class LegacyDecoder : Decoder + { + public static void Register() + { + AddDecoder(@"osu file format v14", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v13", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v12", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v11", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v10", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v9", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v8", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v7", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v6", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v5", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v4", m => new LegacyBeatmapDecoder(m)); + AddDecoder(@"osu file format v3", m => new LegacyBeatmapDecoder(m)); + // TODO: differences between versions + } + + protected int BeatmapVersion; + protected readonly Dictionary Variables = new Dictionary(); + + public override Decoder GetStoryboardDecoder() => new LegacyStoryboardDecoder(BeatmapVersion); + + public override Beatmap DecodeBeatmap(StreamReader stream) => new LegacyBeatmap(base.DecodeBeatmap(stream)); + + protected override void ParseBeatmap(StreamReader stream, Beatmap beatmap) + { + throw new NotImplementedException(); + } + + protected override void ParseStoryboard(StreamReader stream, Storyboard storyboard) + { + throw new NotImplementedException(); + } + + protected void ParseContent(StreamReader stream) + { + Section section = Section.None; + + string line; + while ((line = stream.ReadLine()) != null) + { + if (ShouldSkipLine(line)) + continue; + + // It's already set in ParseBeatmap... why do it again? + //if (line.StartsWith(@"osu file format v")) + //{ + // Beatmap.BeatmapInfo.BeatmapVersion = int.Parse(line.Substring(17)); + // continue; + //} + + if (line.StartsWith(@"[") && line.EndsWith(@"]")) + { + if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) + throw new InvalidDataException($@"Unknown osu section {line}"); + continue; + } + + ProcessSection(section, line); + } + } + + protected virtual bool ShouldSkipLine(string line) + { + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//")) + return true; + return false; + } + + protected abstract void ProcessSection(Section section, string line); + + /// + /// Decodes any beatmap variables present in a line into their real values. + /// + /// The line which may contains variables. + protected void DecodeVariables(ref string line) + { + while (line.IndexOf('$') >= 0) + { + string origLine = line; + string[] split = line.Split(','); + for (int i = 0; i < split.Length; i++) + { + var item = split[i]; + if (item.StartsWith("$") && Variables.ContainsKey(item)) + split[i] = Variables[item]; + } + + line = string.Join(",", split); + if (line == origLine) + break; + } + } + + protected enum Section + { + None, + General, + Editor, + Metadata, + Difficulty, + Events, + TimingPoints, + Colours, + HitObjects, + Variables, + } + + internal enum LegacySampleBank + { + None = 0, + Normal = 1, + Soft = 2, + Drum = 3 + } + + internal enum EventType + { + Background = 0, + Video = 1, + Break = 2, + Colour = 3, + Sprite = 4, + Sample = 5, + Animation = 6 + } + + internal enum LegacyOrigins + { + TopLeft, + Centre, + CentreLeft, + TopRight, + BottomCentre, + TopCentre, + Custom, + CentreRight, + BottomLeft, + BottomRight + }; + + internal enum StoryLayer + { + Background = 0, + Fail = 1, + Pass = 2, + Foreground = 3 + } + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs new file mode 100644 index 0000000000..8da6a0cefb --- /dev/null +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -0,0 +1,271 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Globalization; +using System.IO; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.IO.File; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps.Formats +{ + public class LegacyStoryboardDecoder : LegacyDecoder + { + private Storyboard storyboard; + + private StoryboardSprite storyboardSprite; + private CommandTimelineGroup timelineGroup; + + public LegacyStoryboardDecoder() + { + } + + public LegacyStoryboardDecoder(int beatmapVersion) + { + BeatmapVersion = beatmapVersion; + } + + protected override void ParseStoryboard(StreamReader stream, Storyboard storyboard) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + if (storyboard == null) + throw new ArgumentNullException(nameof(storyboard)); + + this.storyboard = storyboard; + + ParseContent(stream); + } + + protected override void ProcessSection(Section section, string line) + { + switch (section) + { + case Section.Events: + handleEvents(line); + break; + } + } + + private void handleEvents(string line) + { + var depth = 0; + while (line.StartsWith(" ") || line.StartsWith("_")) + { + ++depth; + line = line.Substring(1); + } + + DecodeVariables(ref line); + + string[] split = line.Split(','); + + if (depth == 0) + { + storyboardSprite = null; + + EventType type; + if (!Enum.TryParse(split[0], out type)) + throw new InvalidDataException($@"Unknown event type {split[0]}"); + + switch (type) + { + case EventType.Sprite: + { + var layer = parseLayer(split[1]); + var origin = parseOrigin(split[2]); + var path = cleanFilename(split[3]); + var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); + var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); + storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y)); + storyboard.GetLayer(layer).Add(storyboardSprite); + } + break; + case EventType.Animation: + { + var layer = parseLayer(split[1]); + var origin = parseOrigin(split[2]); + var path = cleanFilename(split[3]); + var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); + var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); + var frameCount = int.Parse(split[6]); + var frameDelay = double.Parse(split[7], NumberFormatInfo.InvariantInfo); + var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever; + storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType); + storyboard.GetLayer(layer).Add(storyboardSprite); + } + break; + case EventType.Sample: + { + var time = double.Parse(split[1], CultureInfo.InvariantCulture); + var layer = parseLayer(split[2]); + var path = cleanFilename(split[3]); + var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; + storyboard.GetLayer(layer).Add(new StoryboardSample(path, time, volume)); + } + break; + } + } + else + { + if (depth < 2) + timelineGroup = storyboardSprite?.TimelineGroup; + + var commandType = split[0]; + switch (commandType) + { + case "T": + { + var triggerName = split[1]; + var startTime = split.Length > 2 ? double.Parse(split[2], CultureInfo.InvariantCulture) : double.MinValue; + var endTime = split.Length > 3 ? double.Parse(split[3], CultureInfo.InvariantCulture) : double.MaxValue; + var groupNumber = split.Length > 4 ? int.Parse(split[4]) : 0; + timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber); + } + break; + case "L": + { + var startTime = double.Parse(split[1], CultureInfo.InvariantCulture); + var loopCount = int.Parse(split[2]); + timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); + } + break; + default: + { + if (string.IsNullOrEmpty(split[3])) + split[3] = split[2]; + + var easing = (Easing)int.Parse(split[1]); + var startTime = double.Parse(split[2], CultureInfo.InvariantCulture); + var endTime = double.Parse(split[3], CultureInfo.InvariantCulture); + + switch (commandType) + { + case "F": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue); + } + break; + case "S": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue)); + } + break; + case "V": + { + var startX = float.Parse(split[4], CultureInfo.InvariantCulture); + var startY = float.Parse(split[5], CultureInfo.InvariantCulture); + var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; + var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); + } + break; + case "R": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Rotation.Add(easing, startTime, endTime, MathHelper.RadiansToDegrees(startValue), MathHelper.RadiansToDegrees(endValue)); + } + break; + case "M": + { + var startX = float.Parse(split[4], CultureInfo.InvariantCulture); + var startY = float.Parse(split[5], CultureInfo.InvariantCulture); + var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; + var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + timelineGroup?.X.Add(easing, startTime, endTime, startX, endX); + timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY); + } + break; + case "MX": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue); + } + break; + case "MY": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue); + } + break; + case "C": + { + var startRed = float.Parse(split[4], CultureInfo.InvariantCulture); + var startGreen = float.Parse(split[5], CultureInfo.InvariantCulture); + var startBlue = float.Parse(split[6], CultureInfo.InvariantCulture); + var endRed = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startRed; + var endGreen = split.Length > 8 ? float.Parse(split[8], CultureInfo.InvariantCulture) : startGreen; + var endBlue = split.Length > 9 ? float.Parse(split[9], CultureInfo.InvariantCulture) : startBlue; + timelineGroup?.Colour.Add(easing, startTime, endTime, + new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), + new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); + } + break; + case "P": + { + var type = split[4]; + switch (type) + { + case "A": + timelineGroup?.BlendingMode.Add(easing, startTime, endTime, BlendingMode.Additive, startTime == endTime ? BlendingMode.Additive : BlendingMode.Inherit); + break; + case "H": + timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime); + break; + case "V": + timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime); + break; + } + } + break; + default: + throw new InvalidDataException($@"Unknown command type: {commandType}"); + } + } + break; + } + } + } + + private string parseLayer(string value) => Enum.Parse(typeof(StoryLayer), value).ToString(); + + private Anchor parseOrigin(string value) + { + var origin = (LegacyOrigins)Enum.Parse(typeof(LegacyOrigins), value); + switch (origin) + { + case LegacyOrigins.TopLeft: + return Anchor.TopLeft; + case LegacyOrigins.TopCentre: + return Anchor.TopCentre; + case LegacyOrigins.TopRight: + return Anchor.TopRight; + case LegacyOrigins.CentreLeft: + return Anchor.CentreLeft; + case LegacyOrigins.Centre: + return Anchor.Centre; + case LegacyOrigins.CentreRight: + return Anchor.CentreRight; + case LegacyOrigins.BottomLeft: + return Anchor.BottomLeft; + case LegacyOrigins.BottomCentre: + return Anchor.BottomCentre; + case LegacyOrigins.BottomRight: + return Anchor.BottomRight; + } + throw new InvalidDataException($@"Unknown origin: {value}"); + } + + private string cleanFilename(string path) => FileSafety.PathStandardise(path.Trim('\"')); + } +} diff --git a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs deleted file mode 100644 index 21fee0f465..0000000000 --- a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs +++ /dev/null @@ -1,730 +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.Globalization; -using System.IO; -using OpenTK.Graphics; -using osu.Game.Beatmaps.Timing; -using osu.Game.Beatmaps.Legacy; -using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Storyboards; -using OpenTK; -using osu.Framework.Graphics; -using osu.Framework.IO.File; - -namespace osu.Game.Beatmaps.Formats -{ - public class OsuLegacyDecoder : BeatmapDecoder - { - public static void Register() - { - AddDecoder(@"osu file format v14"); - AddDecoder(@"osu file format v13"); - AddDecoder(@"osu file format v12"); - AddDecoder(@"osu file format v11"); - AddDecoder(@"osu file format v10"); - AddDecoder(@"osu file format v9"); - AddDecoder(@"osu file format v8"); - AddDecoder(@"osu file format v7"); - AddDecoder(@"osu file format v6"); - AddDecoder(@"osu file format v5"); - AddDecoder(@"osu file format v4"); - AddDecoder(@"osu file format v3"); - // TODO: differences between versions - } - - private ConvertHitObjectParser parser; - - private readonly Dictionary variables = new Dictionary(); - - private LegacySampleBank defaultSampleBank; - private int defaultSampleVolume = 100; - - private readonly int beatmapVersion; - - public OsuLegacyDecoder() - { - } - - public OsuLegacyDecoder(string header) - { - beatmapVersion = int.Parse(header.Substring(17)); - } - - private enum Section - { - None, - General, - Editor, - Metadata, - Difficulty, - Events, - TimingPoints, - Colours, - HitObjects, - Variables, - } - - private void handleGeneral(Beatmap beatmap, string line) - { - var pair = splitKeyVal(line, ':'); - - var metadata = beatmap.BeatmapInfo.Metadata; - switch (pair.Key) - { - case @"AudioFilename": - metadata.AudioFile = pair.Value; - break; - case @"AudioLeadIn": - beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value); - break; - case @"PreviewTime": - metadata.PreviewTime = int.Parse(pair.Value); - break; - case @"Countdown": - beatmap.BeatmapInfo.Countdown = int.Parse(pair.Value) == 1; - break; - case @"SampleSet": - defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value); - break; - case @"SampleVolume": - defaultSampleVolume = int.Parse(pair.Value); - break; - case @"StackLeniency": - beatmap.BeatmapInfo.StackLeniency = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"Mode": - beatmap.BeatmapInfo.RulesetID = int.Parse(pair.Value); - - switch (beatmap.BeatmapInfo.RulesetID) - { - case 0: - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); - break; - case 1: - parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(); - break; - case 2: - parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(); - break; - case 3: - parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(); - break; - } - break; - case @"LetterboxInBreaks": - beatmap.BeatmapInfo.LetterboxInBreaks = int.Parse(pair.Value) == 1; - break; - case @"SpecialStyle": - beatmap.BeatmapInfo.SpecialStyle = int.Parse(pair.Value) == 1; - break; - case @"WidescreenStoryboard": - beatmap.BeatmapInfo.WidescreenStoryboard = int.Parse(pair.Value) == 1; - break; - } - } - - private void handleEditor(Beatmap beatmap, string line) - { - var pair = splitKeyVal(line, ':'); - - switch (pair.Key) - { - case @"Bookmarks": - beatmap.BeatmapInfo.StoredBookmarks = pair.Value; - break; - case @"DistanceSpacing": - beatmap.BeatmapInfo.DistanceSpacing = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"BeatDivisor": - beatmap.BeatmapInfo.BeatDivisor = int.Parse(pair.Value); - break; - case @"GridSize": - beatmap.BeatmapInfo.GridSize = int.Parse(pair.Value); - break; - case @"TimelineZoom": - beatmap.BeatmapInfo.TimelineZoom = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - } - } - - private void handleMetadata(Beatmap beatmap, string line) - { - var pair = splitKeyVal(line, ':'); - - var metadata = beatmap.BeatmapInfo.Metadata; - switch (pair.Key) - { - case @"Title": - metadata.Title = pair.Value; - break; - case @"TitleUnicode": - metadata.TitleUnicode = pair.Value; - break; - case @"Artist": - metadata.Artist = pair.Value; - break; - case @"ArtistUnicode": - metadata.ArtistUnicode = pair.Value; - break; - case @"Creator": - metadata.Author = pair.Value; - break; - case @"Version": - beatmap.BeatmapInfo.Version = pair.Value; - break; - case @"Source": - beatmap.BeatmapInfo.Metadata.Source = pair.Value; - break; - case @"Tags": - beatmap.BeatmapInfo.Metadata.Tags = pair.Value; - break; - case @"BeatmapID": - beatmap.BeatmapInfo.OnlineBeatmapID = int.Parse(pair.Value); - break; - case @"BeatmapSetID": - beatmap.BeatmapInfo.OnlineBeatmapSetID = int.Parse(pair.Value); - metadata.OnlineBeatmapSetID = int.Parse(pair.Value); - break; - } - } - - private void handleDifficulty(Beatmap beatmap, string line) - { - var pair = splitKeyVal(line, ':'); - - var difficulty = beatmap.BeatmapInfo.Difficulty; - switch (pair.Key) - { - case @"HPDrainRate": - difficulty.DrainRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"CircleSize": - difficulty.CircleSize = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"OverallDifficulty": - difficulty.OverallDifficulty = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"ApproachRate": - difficulty.ApproachRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"SliderMultiplier": - difficulty.SliderMultiplier = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"SliderTickRate": - difficulty.SliderTickRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - } - } - - /// - /// Decodes any beatmap variables present in a line into their real values. - /// - /// The line which may contains variables. - private void decodeVariables(ref string line) - { - while (line.IndexOf('$') >= 0) - { - string origLine = line; - string[] split = line.Split(','); - for (int i = 0; i < split.Length; i++) - { - var item = split[i]; - if (item.StartsWith("$") && variables.ContainsKey(item)) - split[i] = variables[item]; - } - - line = string.Join(",", split); - if (line == origLine) break; - } - } - - private void handleEvents(Beatmap beatmap, string line, ref StoryboardSprite storyboardSprite, ref CommandTimelineGroup timelineGroup) - { - var depth = 0; - while (line.StartsWith(" ") || line.StartsWith("_")) - { - ++depth; - line = line.Substring(1); - } - - decodeVariables(ref line); - - string[] split = line.Split(','); - - if (depth == 0) - { - storyboardSprite = null; - - EventType type; - if (!Enum.TryParse(split[0], out type)) - throw new InvalidDataException($@"Unknown event type {split[0]}"); - - switch (type) - { - case EventType.Video: - case EventType.Background: - string filename = split[2].Trim('"'); - - if (type == EventType.Background) - beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; - - break; - case EventType.Break: - var breakEvent = new BreakPeriod - { - StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo), - EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo) - }; - - if (!breakEvent.HasEffect) - return; - - beatmap.Breaks.Add(breakEvent); - break; - case EventType.Sprite: - { - var layer = parseLayer(split[1]); - var origin = parseOrigin(split[2]); - var path = cleanFilename(split[3]); - var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); - var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); - storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y)); - beatmap.Storyboard.GetLayer(layer).Add(storyboardSprite); - } - break; - case EventType.Animation: - { - var layer = parseLayer(split[1]); - var origin = parseOrigin(split[2]); - var path = cleanFilename(split[3]); - var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); - var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); - var frameCount = int.Parse(split[6]); - var frameDelay = double.Parse(split[7], NumberFormatInfo.InvariantInfo); - var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever; - storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType); - beatmap.Storyboard.GetLayer(layer).Add(storyboardSprite); - } - break; - case EventType.Sample: - { - var time = double.Parse(split[1], CultureInfo.InvariantCulture); - var layer = parseLayer(split[2]); - var path = cleanFilename(split[3]); - var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; - beatmap.Storyboard.GetLayer(layer).Add(new StoryboardSample(path, time, volume)); - } - break; - } - } - else - { - if (depth < 2) - timelineGroup = storyboardSprite?.TimelineGroup; - - var commandType = split[0]; - switch (commandType) - { - case "T": - { - var triggerName = split[1]; - var startTime = split.Length > 2 ? double.Parse(split[2], CultureInfo.InvariantCulture) : double.MinValue; - var endTime = split.Length > 3 ? double.Parse(split[3], CultureInfo.InvariantCulture) : double.MaxValue; - var groupNumber = split.Length > 4 ? int.Parse(split[4]) : 0; - timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber); - } - break; - case "L": - { - var startTime = double.Parse(split[1], CultureInfo.InvariantCulture); - var loopCount = int.Parse(split[2]); - timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); - } - break; - default: - { - if (string.IsNullOrEmpty(split[3])) - split[3] = split[2]; - - var easing = (Easing)int.Parse(split[1]); - var startTime = double.Parse(split[2], CultureInfo.InvariantCulture); - var endTime = double.Parse(split[3], CultureInfo.InvariantCulture); - - switch (commandType) - { - case "F": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue); - } - break; - case "S": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue)); - } - break; - case "V": - { - var startX = float.Parse(split[4], CultureInfo.InvariantCulture); - var startY = float.Parse(split[5], CultureInfo.InvariantCulture); - var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; - var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; - timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); - } - break; - case "R": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Rotation.Add(easing, startTime, endTime, MathHelper.RadiansToDegrees(startValue), MathHelper.RadiansToDegrees(endValue)); - } - break; - case "M": - { - var startX = float.Parse(split[4], CultureInfo.InvariantCulture); - var startY = float.Parse(split[5], CultureInfo.InvariantCulture); - var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; - var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; - timelineGroup?.X.Add(easing, startTime, endTime, startX, endX); - timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY); - } - break; - case "MX": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue); - } - break; - case "MY": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue); - } - break; - case "C": - { - var startRed = float.Parse(split[4], CultureInfo.InvariantCulture); - var startGreen = float.Parse(split[5], CultureInfo.InvariantCulture); - var startBlue = float.Parse(split[6], CultureInfo.InvariantCulture); - var endRed = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startRed; - var endGreen = split.Length > 8 ? float.Parse(split[8], CultureInfo.InvariantCulture) : startGreen; - var endBlue = split.Length > 9 ? float.Parse(split[9], CultureInfo.InvariantCulture) : startBlue; - timelineGroup?.Colour.Add(easing, startTime, endTime, - new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), - new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); - } - break; - case "P": - { - var type = split[4]; - switch (type) - { - case "A": timelineGroup?.BlendingMode.Add(easing, startTime, endTime, BlendingMode.Additive, startTime == endTime ? BlendingMode.Additive : BlendingMode.Inherit); break; - case "H": timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime); break; - case "V": timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime); break; - } - } - break; - default: - throw new InvalidDataException($@"Unknown command type: {commandType}"); - } - } - break; - } - } - } - - private static string cleanFilename(string path) - => FileSafety.PathStandardise(path.Trim('\"')); - - private static Anchor parseOrigin(string value) - { - var origin = (LegacyOrigins)Enum.Parse(typeof(LegacyOrigins), value); - switch (origin) - { - case LegacyOrigins.TopLeft: return Anchor.TopLeft; - case LegacyOrigins.TopCentre: return Anchor.TopCentre; - case LegacyOrigins.TopRight: return Anchor.TopRight; - case LegacyOrigins.CentreLeft: return Anchor.CentreLeft; - case LegacyOrigins.Centre: return Anchor.Centre; - case LegacyOrigins.CentreRight: return Anchor.CentreRight; - case LegacyOrigins.BottomLeft: return Anchor.BottomLeft; - case LegacyOrigins.BottomCentre: return Anchor.BottomCentre; - case LegacyOrigins.BottomRight: return Anchor.BottomRight; - } - throw new InvalidDataException($@"Unknown origin: {value}"); - } - - private static string parseLayer(string value) - => Enum.Parse(typeof(StoryLayer), value).ToString(); - - private void handleTimingPoints(Beatmap beatmap, string line) - { - string[] split = line.Split(','); - - double time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo); - double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); - double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; - - TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; - if (split.Length >= 3) - timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)int.Parse(split[2]); - - LegacySampleBank sampleSet = defaultSampleBank; - if (split.Length >= 4) - sampleSet = (LegacySampleBank)int.Parse(split[3]); - - //SampleBank sampleBank = SampleBank.Default; - //if (split.Length >= 5) - // sampleBank = (SampleBank)int.Parse(split[4]); - - int sampleVolume = defaultSampleVolume; - if (split.Length >= 6) - sampleVolume = int.Parse(split[5]); - - bool timingChange = true; - if (split.Length >= 7) - timingChange = split[6][0] == '1'; - - bool kiaiMode = false; - bool omitFirstBarSignature = false; - if (split.Length >= 8) - { - int effectFlags = int.Parse(split[7]); - kiaiMode = (effectFlags & 1) > 0; - omitFirstBarSignature = (effectFlags & 8) > 0; - } - - string stringSampleSet = sampleSet.ToString().ToLower(); - if (stringSampleSet == @"none") - stringSampleSet = @"normal"; - - DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(time); - SoundControlPoint soundPoint = beatmap.ControlPointInfo.SoundPointAt(time); - EffectControlPoint effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); - - if (timingChange) - { - beatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint - { - Time = time, - BeatLength = beatLength, - TimeSignature = timeSignature - }); - } - - if (speedMultiplier != difficultyPoint.SpeedMultiplier) - { - beatmap.ControlPointInfo.DifficultyPoints.RemoveAll(x => x.Time == time); - beatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint - { - Time = time, - SpeedMultiplier = speedMultiplier - }); - } - - if (stringSampleSet != soundPoint.SampleBank || sampleVolume != soundPoint.SampleVolume) - { - beatmap.ControlPointInfo.SoundPoints.Add(new SoundControlPoint - { - Time = time, - SampleBank = stringSampleSet, - SampleVolume = sampleVolume - }); - } - - if (kiaiMode != effectPoint.KiaiMode || omitFirstBarSignature != effectPoint.OmitFirstBarLine) - { - beatmap.ControlPointInfo.EffectPoints.Add(new EffectControlPoint - { - Time = time, - KiaiMode = kiaiMode, - OmitFirstBarLine = omitFirstBarSignature - }); - } - } - - private void handleColours(Beatmap beatmap, string line, ref bool hasCustomColours) - { - var pair = splitKeyVal(line, ':'); - - string[] split = pair.Value.Split(','); - - if (split.Length != 3) - throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B): {pair.Value}"); - - byte r, g, b; - if (!byte.TryParse(split[0], out r) || !byte.TryParse(split[1], out g) || !byte.TryParse(split[2], out b)) - throw new InvalidOperationException(@"Color must be specified with 8-bit integer components"); - - if (!hasCustomColours) - { - beatmap.ComboColors.Clear(); - hasCustomColours = true; - } - - // Note: the combo index specified in the beatmap is discarded - if (pair.Key.StartsWith(@"Combo")) - { - beatmap.ComboColors.Add(new Color4 - { - R = r / 255f, - G = g / 255f, - B = b / 255f, - A = 1f, - }); - } - } - - private void handleVariables(string line) - { - var pair = splitKeyVal(line, '='); - variables[pair.Key] = pair.Value; - } - - protected override Beatmap ParseFile(StreamReader stream) - { - return new LegacyBeatmap(base.ParseFile(stream)); - } - - public override Beatmap Decode(StreamReader stream) - { - return new LegacyBeatmap(base.Decode(stream)); - } - - protected override void ParseFile(StreamReader stream, Beatmap beatmap) - { - beatmap.BeatmapInfo.BeatmapVersion = beatmapVersion; - - Section section = Section.None; - bool hasCustomColours = false; - StoryboardSprite storyboardSprite = null; - CommandTimelineGroup timelineGroup = null; - - string line; - while ((line = stream.ReadLine()) != null) - { - if (string.IsNullOrEmpty(line)) - continue; - - if (line.StartsWith("//")) - continue; - - if (line.StartsWith(@"osu file format v")) - { - beatmap.BeatmapInfo.BeatmapVersion = int.Parse(line.Substring(17)); - continue; - } - - if (line.StartsWith(@"[") && line.EndsWith(@"]")) - { - if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) - throw new InvalidDataException($@"Unknown osu section {line}"); - continue; - } - - switch (section) - { - case Section.General: - handleGeneral(beatmap, line); - break; - case Section.Editor: - handleEditor(beatmap, line); - break; - case Section.Metadata: - handleMetadata(beatmap, line); - break; - case Section.Difficulty: - handleDifficulty(beatmap, line); - break; - case Section.Events: - handleEvents(beatmap, line, ref storyboardSprite, ref timelineGroup); - break; - case Section.TimingPoints: - handleTimingPoints(beatmap, line); - break; - case Section.Colours: - 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) - beatmap.HitObjects.Add(obj); - - break; - case Section.Variables: - handleVariables(line); - break; - } - } - - foreach (var hitObject in beatmap.HitObjects) - hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.Difficulty); - } - - private KeyValuePair splitKeyVal(string line, char separator) - { - return new KeyValuePair - ( - line.Remove(line.IndexOf(separator)).Trim(), - line.Substring(line.IndexOf(separator) + 1).Trim() - ); - } - - internal enum LegacySampleBank - { - None = 0, - Normal = 1, - Soft = 2, - Drum = 3 - } - - internal enum EventType - { - Background = 0, - Video = 1, - Break = 2, - Colour = 3, - Sprite = 4, - Sample = 5, - Animation = 6 - } - - internal enum LegacyOrigins - { - TopLeft, - Centre, - CentreLeft, - TopRight, - BottomCentre, - TopCentre, - Custom, - CentreRight, - BottomLeft, - BottomRight - }; - - internal enum StoryLayer - { - Background = 0, - Fail = 1, - Pass = 2, - Foreground = 3 - } - } -} diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index fb307b7144..0cf4a0c65b 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -8,7 +8,7 @@ namespace osu.Game.Beatmaps.Timing /// /// The minimum duration required for a break to have any effect. /// - private const double min_break_duration = 650; + public const double MIN_BREAK_DURATION = 650; /// /// The break start time. @@ -28,6 +28,6 @@ namespace osu.Game.Beatmaps.Timing /// /// Whether the break has any effect. Breaks that are too short are culled before they are added to the beatmap. /// - public bool HasEffect => Duration >= min_break_duration; + public bool HasEffect => Duration >= MIN_BREAK_DURATION; } -} +} \ No newline at end of file diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 277846ee80..8c8cf212c1 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -8,6 +8,12 @@ using osu.Game.Rulesets.Mods; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using osu.Game.Storyboards; +using osu.Framework.IO.File; +using System.IO; +using osu.Game.IO.Serialization; +using System.Diagnostics; namespace osu.Game.Beatmaps { @@ -28,11 +34,106 @@ namespace osu.Game.Beatmaps Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); Mods.ValueChanged += mods => applyRateAdjustments(); + + beatmap = new AsyncLazy(populateBeatmap); + background = new AsyncLazy(populateBackground, b => b == null || !b.IsDisposed); + track = new AsyncLazy(populateTrack); + waveform = new AsyncLazy(populateWaveform); + storyboard = new AsyncLazy(populateStoryboard); } - private void applyRateAdjustments() + /// + /// Saves the . + /// + public void Save() { - var t = track; + var path = FileSafety.GetTempPath(Guid.NewGuid().ToString().Replace("-", string.Empty) + ".json"); + using (var sw = new StreamWriter(path)) + sw.WriteLine(Beatmap.Serialize()); + Process.Start(path); + } + + protected abstract Beatmap GetBeatmap(); + protected abstract Texture GetBackground(); + protected abstract Track GetTrack(); + protected virtual Waveform GetWaveform() => new Waveform(); + protected virtual Storyboard GetStoryboard() => new Storyboard(); + + public bool BeatmapLoaded => beatmap.IsResultAvailable; + public Beatmap Beatmap => beatmap.Value.Result; + public async Task GetBeatmapAsync() => await beatmap.Value; + + private readonly AsyncLazy beatmap; + + private Beatmap populateBeatmap() + { + var b = GetBeatmap() ?? new Beatmap(); + + // use the database-backed info. + b.BeatmapInfo = BeatmapInfo; + + return b; + } + + public bool BackgroundLoaded => background.IsResultAvailable; + public Texture Background => background.Value.Result; + public async Task GetBackgroundAsync() => await background.Value; + private AsyncLazy background; + + private Texture populateBackground() => GetBackground(); + + public bool TrackLoaded => track.IsResultAvailable; + public Track Track => track.Value.Result; + public async Task GetTrackAsync() => await track.Value; + private AsyncLazy track; + + private Track populateTrack() + { + // we want to ensure that we always have a track, even if it's a fake one. + var t = GetTrack() ?? new TrackVirtual(); + applyRateAdjustments(t); + return t; + } + + public bool WaveformLoaded => waveform.IsResultAvailable; + public Waveform Waveform => waveform.Value.Result; + public async Task GetWaveformAsync() => await waveform.Value; + private readonly AsyncLazy waveform; + + private Waveform populateWaveform() => GetWaveform(); + + public bool StoryboardLoaded => storyboard.IsResultAvailable; + public Storyboard Storyboard => storyboard.Value.Result; + public async Task GetStoryboardAsync() => await storyboard.Value; + private readonly AsyncLazy storyboard; + + private Storyboard populateStoryboard() => GetStoryboard(); + + public void TransferTo(WorkingBeatmap other) + { + if (track.IsResultAvailable && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) + other.track = track; + + if (background.IsResultAvailable && Background != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) + other.background = background; + } + + public virtual void Dispose() + { + if (BackgroundLoaded) Background?.Dispose(); + if (WaveformLoaded) Waveform?.Dispose(); + if (StoryboardLoaded) Storyboard?.Dispose(); + } + + /// + /// Eagerly dispose of the audio track associated with this (if any). + /// Accessing track again will load a fresh instance. + /// + public void RecycleTrack() => track.Recycle(); + + private void applyRateAdjustments(Track t = null) + { + if (t == null && track.IsResultAvailable) t = Track; if (t == null) return; t.ResetSpeedAdjustments(); @@ -40,89 +141,65 @@ namespace osu.Game.Beatmaps mod.ApplyToClock(t); } - protected abstract Beatmap GetBeatmap(); - protected abstract Texture GetBackground(); - protected abstract Track GetTrack(); - - private Beatmap beatmap; - private readonly object beatmapLock = new object(); - public Beatmap Beatmap + public class AsyncLazy { - get + private Lazy> lazy; + private readonly Func valueFactory; + private readonly Func stillValidFunction; + + private readonly object initLock = new object(); + + public AsyncLazy(Func valueFactory, Func stillValidFunction = null) { - lock (beatmapLock) + this.valueFactory = valueFactory; + this.stillValidFunction = stillValidFunction; + + recreate(); + } + + public void Recycle() + { + if (!IsResultAvailable) return; + + (lazy.Value.Result as IDisposable)?.Dispose(); + recreate(); + } + + public bool IsResultAvailable + { + get { - if (beatmap != null) return beatmap; - - beatmap = GetBeatmap() ?? new Beatmap(); - - // use the database-backed info. - beatmap.BeatmapInfo = BeatmapInfo; - - return beatmap; + recreateIfInvalid(); + return lazy.Value.IsCompleted; } } - } - private readonly object backgroundLock = new object(); - private Texture background; - public Texture Background - { - get + public Task Value { - lock (backgroundLock) + get { - return background ?? (background = GetBackground()); + recreateIfInvalid(); + return lazy.Value; } } - } - private Track track; - private readonly object trackLock = new object(); - public Track Track - { - get + private void recreateIfInvalid() { - lock (trackLock) + lock (initLock) { - if (track != null) return track; + if (!lazy.IsValueCreated || !lazy.Value.IsCompleted) + // we have not yet been initialised or haven't run the task. + return; - // we want to ensure that we always have a track, even if it's a fake one. - track = GetTrack() ?? new TrackVirtual(); + if (stillValidFunction?.Invoke(lazy.Value.Result) ?? true) + // we are still in a valid state. + return; - applyRateAdjustments(); - return track; + recreate(); } } - } - public bool TrackLoaded => track != null; - - public void TransferTo(WorkingBeatmap other) - { - lock (trackLock) - { - if (track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) - other.track = track; - } - - if (background != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) - other.background = background; - } - - public virtual void Dispose() - { - background?.Dispose(); - background = null; - } - - public void DisposeTrack() - { - lock (trackLock) - { - track?.Dispose(); - track = null; - } + private void recreate() => lazy = new Lazy>(() => Task.Run(valueFactory)); } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index b000f08369..f4c7bdb586 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -16,10 +16,11 @@ namespace osu.Game.Configuration Set(OsuSetting.Ruleset, 0, 0, int.MaxValue); Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details); - Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10); - Set(OsuSetting.DisplayStarsMaximum, 10.0, 0, 10); + Set(OsuSetting.ShowConvertedBeatmaps, true); + Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); + Set(OsuSetting.DisplayStarsMaximum, 10.0, 0, 10, 0.1); - Set(OsuSetting.SelectionRandomType, SelectionRandomType.RandomPermutation); + Set(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation); Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2, 1); @@ -41,11 +42,11 @@ namespace osu.Game.Configuration Set(OsuSetting.MenuVoice, true); Set(OsuSetting.MenuMusic, true); - Set(OsuSetting.AudioOffset, 0, -500.0, 500.0); + Set(OsuSetting.AudioOffset, 0, -500.0, 500.0, 1); // Input - Set(OsuSetting.MenuCursorSize, 1.0, 0.5f, 2); - Set(OsuSetting.GameplayCursorSize, 1.0, 0.5f, 2); + Set(OsuSetting.MenuCursorSize, 1.0, 0.5f, 2, 0.01); + Set(OsuSetting.GameplayCursorSize, 1.0, 0.5f, 2, 0.01); Set(OsuSetting.AutoCursorSize, false); Set(OsuSetting.MouseDisableButtons, false); @@ -63,13 +64,12 @@ namespace osu.Game.Configuration Set(OsuSetting.SnakingOutSliders, true); // Gameplay - Set(OsuSetting.DimLevel, 0.3, 0, 1); + Set(OsuSetting.DimLevel, 0.3, 0, 1, 0.01); Set(OsuSetting.ShowInterface, true); Set(OsuSetting.KeyOverlay, false); Set(OsuSetting.FloatingComments, false); - Set(OsuSetting.PlaybackSpeed, 1.0, 0.5f, 2); // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -93,7 +93,6 @@ namespace osu.Game.Configuration ShowStoryboard, KeyOverlay, FloatingComments, - PlaybackSpeed, ShowInterface, MouseDisableButtons, MouseDisableWheel, @@ -109,11 +108,12 @@ namespace osu.Game.Configuration SaveUsername, DisplayStarsMinimum, DisplayStarsMaximum, - SelectionRandomType, + RandomSelectAlgorithm, SnakingInSliders, SnakingOutSliders, ShowFpsDisplay, ChatDisplayHeight, - Version + Version, + ShowConvertedBeatmaps } } diff --git a/osu.Game/Configuration/SelectionRandomType.cs b/osu.Game/Configuration/RandomSelectAlgorithm.cs similarity index 86% rename from osu.Game/Configuration/SelectionRandomType.cs rename to osu.Game/Configuration/RandomSelectAlgorithm.cs index 298ee71e36..cde657dba8 100644 --- a/osu.Game/Configuration/SelectionRandomType.cs +++ b/osu.Game/Configuration/RandomSelectAlgorithm.cs @@ -5,11 +5,11 @@ using System.ComponentModel; namespace osu.Game.Configuration { - public enum SelectionRandomType + public enum RandomSelectAlgorithm { [Description("Never repeat")] RandomPermutation, [Description("Random")] Random } -} \ No newline at end of file +} diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index d8e2e35bd7..d8c3ce6694 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -4,128 +4,60 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using osu.Framework.Logging; +using System.Threading; +using Microsoft.EntityFrameworkCore; 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; + /// + /// Create a new instance (separate from the shared context via for performing isolated operations. + /// + protected readonly Func CreateContext; - protected DatabaseBackedStore(SQLiteConnection connection, Storage storage = null) + private readonly ThreadLocal queryContext; + + /// + /// Refresh an instance potentially from a different thread with a local context-tracked instance. + /// + /// The object to use as a reference when negotiating a local instance. + /// An optional lookup source which will be used to query and populate a freshly retrieved replacement. If not provided, the refreshed object will still be returned but will not have any includes. + /// A valid EF-stored type. + protected virtual void Refresh(ref T obj, IEnumerable lookupSource = null) where T : class, IHasPrimaryKey { + var context = GetContext(); + + if (context.Entry(obj).State != EntityState.Detached) return; + + var id = obj.ID; + obj = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); + context.Entry(obj).Reload(); + } + + /// + /// Retrieve a shared context for performing lookups (or write operations on the update thread, for now). + /// + protected OsuDbContext GetContext() => queryContext.Value; + + protected DatabaseBackedStore(Func createContext, Storage storage = null) + { + CreateContext = createContext; + + // todo: while this seems to work quite well, we need to consider that contexts could enter a state where they are never cleaned up. + queryContext = new ThreadLocal(CreateContext); + 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. + /// Perform any common clean-up tasks. Should be run when idle, or whenever necessary. /// - /// 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) + public virtual void Cleanup() { } - - /// - /// 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/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs new file mode 100644 index 0000000000..6154016083 --- /dev/null +++ b/osu.Game/Database/DatabaseContextFactory.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.Platform; + +namespace osu.Game.Database +{ + public class DatabaseContextFactory + { + private readonly GameHost host; + + private const string database_name = @"client"; + + public DatabaseContextFactory(GameHost host) + { + this.host = host; + } + + public OsuDbContext GetContext() => new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name)); + + public void ResetDatabase() + { + // todo: we probably want to make sure there are no active contexts before performing this operation. + host.Storage.DeleteDatabase(database_name); + } + } +} diff --git a/osu.Game/Database/StoreVersion.cs b/osu.Game/Database/IHasPrimaryKey.cs similarity index 51% rename from osu.Game/Database/StoreVersion.cs rename to osu.Game/Database/IHasPrimaryKey.cs index 00314875a6..9af9852b03 100644 --- a/osu.Game/Database/StoreVersion.cs +++ b/osu.Game/Database/IHasPrimaryKey.cs @@ -1,15 +1,10 @@ // 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 + public interface IHasPrimaryKey { - [PrimaryKey] - public string StoreName { get; set; } - - public int Version { get; set; } + int ID { get; set; } } } diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs new file mode 100644 index 0000000000..d6fb9c8c24 --- /dev/null +++ b/osu.Game/Database/OsuDbContext.cs @@ -0,0 +1,285 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Logging; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; +using osu.Game.IO; +using osu.Game.Rulesets; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; + +namespace osu.Game.Database +{ + public class OsuDbContext : DbContext + { + public DbSet BeatmapInfo { get; set; } + public DbSet BeatmapDifficulty { get; set; } + public DbSet BeatmapMetadata { get; set; } + public DbSet BeatmapSetInfo { get; set; } + public DbSet DatabasedKeyBinding { get; set; } + public DbSet FileInfo { get; set; } + public DbSet RulesetInfo { get; set; } + + private readonly string connectionString; + + private static readonly Lazy logger = new Lazy(() => new OsuDbLoggerFactory()); + + static OsuDbContext() + { + // required to initialise native SQLite libraries on some platforms. + SQLitePCL.Batteries_V2.Init(); + } + + /// + /// Create a new in-memory OsuDbContext instance. + /// + public OsuDbContext() + : this("DataSource=:memory:") + { + // required for tooling (see https://wildermuth.com/2017/07/06/Program-cs-in-ASP-NET-Core-2-0). + + Migrate(); + } + + /// + /// Create a new OsuDbContext instance. + /// + /// A valid SQLite connection string. + public OsuDbContext(string connectionString) + { + this.connectionString = connectionString; + + var connection = Database.GetDbConnection(); + connection.Open(); + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA journal_mode=WAL;"; + cmd.ExecuteNonQuery(); + } + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder + // this is required for the time being due to the way we are querying in places like BeatmapStore. + // if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled. + .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning)) + .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10)) + .UseLoggerFactory(logger.Value); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().HasIndex(b => b.OnlineBeatmapID).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.MD5Hash).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); + + modelBuilder.Entity().HasIndex(b => b.OnlineBeatmapSetID).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.DeletePending); + modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); + + modelBuilder.Entity().HasIndex(b => b.Variant); + modelBuilder.Entity().HasIndex(b => b.IntAction); + + modelBuilder.Entity().HasIndex(b => b.Hash).IsUnique(); + modelBuilder.Entity().HasIndex(b => b.ReferenceCount); + + modelBuilder.Entity().HasIndex(b => b.Available); + modelBuilder.Entity().HasIndex(b => b.ShortName).IsUnique(); + + modelBuilder.Entity().HasOne(b => b.BaseDifficulty); + } + + public IDbContextTransaction BeginTransaction() + { + // return Database.BeginTransaction(); + return null; + } + + public int SaveChanges(IDbContextTransaction transaction = null) + { + var ret = base.SaveChanges(); + transaction?.Commit(); + return ret; + } + + private class OsuDbLoggerFactory : ILoggerFactory + { + #region Disposal + + public void Dispose() + { + } + + #endregion + + public ILogger CreateLogger(string categoryName) => new OsuDbLogger(); + + public void AddProvider(ILoggerProvider provider) + { + // no-op. called by tooling. + } + + private class OsuDbLoggerProvider : ILoggerProvider + { + #region Disposal + + public void Dispose() + { + } + + #endregion + + public ILogger CreateLogger(string categoryName) => new OsuDbLogger(); + } + + private class OsuDbLogger : ILogger + { + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + if (logLevel < LogLevel.Information) + return; + + Framework.Logging.LogLevel frameworkLogLevel; + + switch (logLevel) + { + default: + frameworkLogLevel = Framework.Logging.LogLevel.Debug; + break; + case LogLevel.Warning: + frameworkLogLevel = Framework.Logging.LogLevel.Important; + break; + case LogLevel.Error: + case LogLevel.Critical: + frameworkLogLevel = Framework.Logging.LogLevel.Error; + break; + } + + Logger.Log(formatter(state, exception), LoggingTarget.Database, frameworkLogLevel); + } + + public bool IsEnabled(LogLevel logLevel) + { +#if DEBUG_DATABASE + return logLevel > LogLevel.Debug; +#else + return logLevel > LogLevel.Information; +#endif + } + + public IDisposable BeginScope(TState state) => null; + } + } + + public void Migrate() + { + migrateFromSqliteNet(); + + try + { + Database.Migrate(); + } + catch (Exception e) + { + throw new MigrationFailedException(e); + } + } + + private void migrateFromSqliteNet() + { + try + { + // will fail if the database isn't in a sane EF-migrated state. + Database.ExecuteSqlCommand("SELECT MetadataID FROM BeatmapSetInfo LIMIT 1"); + } + catch + { + try + { + Database.ExecuteSqlCommand("DROP TABLE IF EXISTS __EFMigrationsHistory"); + + // will fail (intentionally) if we don't have sqlite-net data present. + Database.ExecuteSqlCommand("SELECT OnlineBeatmapSetId FROM BeatmapMetadata LIMIT 1"); + + try + { + Logger.Log("Performing migration from sqlite-net to EF...", LoggingTarget.Database, Framework.Logging.LogLevel.Important); + + // we are good to perform messy migration of data!. + Database.ExecuteSqlCommand("ALTER TABLE BeatmapDifficulty RENAME TO BeatmapDifficulty_Old"); + Database.ExecuteSqlCommand("ALTER TABLE BeatmapMetadata RENAME TO BeatmapMetadata_Old"); + Database.ExecuteSqlCommand("ALTER TABLE FileInfo RENAME TO FileInfo_Old"); + Database.ExecuteSqlCommand("ALTER TABLE KeyBinding RENAME TO KeyBinding_Old"); + Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetInfo RENAME TO BeatmapSetInfo_Old"); + Database.ExecuteSqlCommand("ALTER TABLE BeatmapInfo RENAME TO BeatmapInfo_Old"); + Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetFileInfo RENAME TO BeatmapSetFileInfo_Old"); + Database.ExecuteSqlCommand("ALTER TABLE RulesetInfo RENAME TO RulesetInfo_Old"); + + Database.ExecuteSqlCommand("DROP TABLE StoreVersion"); + + // perform EF migrations to create sane table structure. + Database.Migrate(); + + // copy data table by table to new structure, dropping old tables as we go. + Database.ExecuteSqlCommand("INSERT INTO FileInfo SELECT * FROM FileInfo_Old"); + Database.ExecuteSqlCommand("DROP TABLE FileInfo_Old"); + + Database.ExecuteSqlCommand("INSERT INTO KeyBinding SELECT ID, [Action], Keys, RulesetID, Variant FROM KeyBinding_Old"); + Database.ExecuteSqlCommand("DROP TABLE KeyBinding_Old"); + + Database.ExecuteSqlCommand( + "INSERT INTO BeatmapMetadata SELECT ID, Artist, ArtistUnicode, AudioFile, Author, BackgroundFile, PreviewTime, Source, Tags, Title, TitleUnicode FROM BeatmapMetadata_Old"); + Database.ExecuteSqlCommand("DROP TABLE BeatmapMetadata_Old"); + + Database.ExecuteSqlCommand( + "INSERT INTO BeatmapDifficulty SELECT `ID`, `ApproachRate`, `CircleSize`, `DrainRate`, `OverallDifficulty`, `SliderMultiplier`, `SliderTickRate` FROM BeatmapDifficulty_Old"); + Database.ExecuteSqlCommand("DROP TABLE BeatmapDifficulty_Old"); + + Database.ExecuteSqlCommand("INSERT INTO BeatmapSetInfo SELECT ID, DeletePending, Hash, BeatmapMetadataID, OnlineBeatmapSetID, Protected FROM BeatmapSetInfo_Old"); + Database.ExecuteSqlCommand("DROP TABLE BeatmapSetInfo_Old"); + + Database.ExecuteSqlCommand("INSERT INTO BeatmapSetFileInfo SELECT ID, BeatmapSetInfoID, FileInfoID, Filename FROM BeatmapSetFileInfo_Old"); + Database.ExecuteSqlCommand("DROP TABLE BeatmapSetFileInfo_Old"); + + Database.ExecuteSqlCommand("INSERT INTO RulesetInfo SELECT ID, Available, InstantiationInfo, Name FROM RulesetInfo_Old"); + Database.ExecuteSqlCommand("DROP TABLE RulesetInfo_Old"); + + Database.ExecuteSqlCommand( + "INSERT INTO BeatmapInfo SELECT ID, AudioLeadIn, BaseDifficultyID, BeatDivisor, BeatmapSetInfoID, Countdown, DistanceSpacing, GridSize, Hash, IFNULL(Hidden, 0), LetterboxInBreaks, MD5Hash, NULLIF(BeatmapMetadataID, 0), NULLIF(OnlineBeatmapID, 0), Path, RulesetID, SpecialStyle, StackLeniency, StarDifficulty, StoredBookmarks, TimelineZoom, Version, WidescreenStoryboard FROM BeatmapInfo_Old"); + Database.ExecuteSqlCommand("DROP TABLE BeatmapInfo_Old"); + + Logger.Log("Migration complete!", LoggingTarget.Database, Framework.Logging.LogLevel.Important); + } + catch (Exception e) + { + throw new MigrationFailedException(e); + } + } + catch (MigrationFailedException) + { + throw; + } + catch + { + } + } + } + } + + public class MigrationFailedException : Exception + { + public MigrationFailedException(Exception exception) + : base("sqlite-net migration failed", exception) + { + } + } +} diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 123ef0662d..d5d75c9e29 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -35,15 +35,18 @@ namespace osu.Game.Graphics.Containers protected override void Update() { - var track = Beatmap.Value.Track; + if (!Beatmap.Value.TrackLoaded || !Beatmap.Value.BeatmapLoaded) return; - if (track == null) + var track = Beatmap.Value.Track; + var beatmap = Beatmap.Value.Beatmap; + + if (track == null || beatmap == null) return; 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); + TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); + EffectControlPoint effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); if (timingPoint.BeatLength == 0) return; @@ -64,7 +67,7 @@ namespace osu.Game.Graphics.Containers return; using (BeginDelayedSequence(-TimeSinceLastBeat, true)) - OnNewBeat(beatIndex, timingPoint, effectPoint, Beatmap.Value.Track.CurrentAmplitudes); + OnNewBeat(beatIndex, timingPoint, effectPoint, track.CurrentAmplitudes); lastBeat = beatIndex; lastTimingPoint = timingPoint; diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs index 11c049ed3e..8df533ad6e 100644 --- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -2,34 +2,39 @@ // 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; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.Containers { public class OsuClickableContainer : ClickableContainer { - protected SampleChannel SampleClick, SampleHover; + private readonly HoverSampleSet sampleSet; + + private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; + + protected override Container Content => content; + + public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Normal) + { + this.sampleSet = sampleSet; + } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { - SampleHover = audio.Sample.Get(@"UI/generic-hover"); - SampleClick = audio.Sample.Get(@"UI/generic-click"); - } + if (AutoSizeAxes != Axes.None) + { + content.RelativeSizeAxes = RelativeSizeAxes; + content.AutoSizeAxes = AutoSizeAxes; + } - protected override bool OnHover(InputState state) - { - SampleHover?.Play(); - return base.OnHover(state); - } - - protected override bool OnClick(InputState state) - { - SampleClick?.Play(); - return base.OnClick(state); + InternalChildren = new Drawable[] + { + content, + new HoverClickSounds(sampleSet) + }; } } } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 4ea4f4cdc3..c788df3066 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -16,8 +16,8 @@ namespace osu.Game.Graphics.Containers [BackgroundDependencyLoader] private void load(AudioManager audio) { - samplePopIn = audio.Sample.Get(@"UI/melodic-5"); - samplePopOut = audio.Sample.Get(@"UI/melodic-4"); + samplePopIn = audio.Sample.Get(@"UI/overlay-pop-in"); + samplePopOut = audio.Sample.Get(@"UI/overlay-pop-out"); StateChanged += onStateChanged; } diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs new file mode 100644 index 0000000000..3f82ad2179 --- /dev/null +++ b/osu.Game/Graphics/Containers/OsuHoverContainer.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 OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Input; + +namespace osu.Game.Graphics.Containers +{ + public class OsuHoverContainer : OsuClickableContainer + { + private Color4 hoverColour; + + protected override bool OnHover(InputState state) + { + this.FadeColour(hoverColour, 500, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + this.FadeColour(Color4.White, 500, Easing.OutQuint); + base.OnHoverLost(state); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + hoverColour = colours.Yellow; + } + } +} diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index e395f1b7bd..3fc9f439fa 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -7,7 +7,7 @@ using OpenTK.Input; namespace osu.Game.Graphics.Containers { - internal class OsuScrollContainer : ScrollContainer + public class OsuScrollContainer : ScrollContainer { /// /// Allows controlling the scroll bar from any position in the container using the right mouse button. diff --git a/osu.Game/Graphics/Containers/ParallaxContainer.cs b/osu.Game/Graphics/Containers/ParallaxContainer.cs index 3e0ed4b059..362563507b 100644 --- a/osu.Game/Graphics/Containers/ParallaxContainer.cs +++ b/osu.Game/Graphics/Containers/ParallaxContainer.cs @@ -11,7 +11,7 @@ using osu.Framework.Configuration; namespace osu.Game.Graphics.Containers { - internal class ParallaxContainer : Container, IRequireHighFrequencyMousePosition + public class ParallaxContainer : Container, IRequireHighFrequencyMousePosition { public float ParallaxAmount = 0.02f; diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 6e5c3c8183..1e2826189b 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -139,6 +139,8 @@ namespace osu.Game.Graphics.Containers public void ScrollTo(Drawable section) => scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); + public void ScrollToTop() => scrollContainer.ScrollTo(0); + private float lastKnownScroll; protected override void UpdateAfterChildren() { diff --git a/osu.Game/Graphics/Processing/RatioAdjust.cs b/osu.Game/Graphics/Processing/RatioAdjust.cs deleted file mode 100644 index 640814d8e1..0000000000 --- a/osu.Game/Graphics/Processing/RatioAdjust.cs +++ /dev/null @@ -1,27 +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.Containers; -using OpenTK; -using osu.Framework.Graphics; - -namespace osu.Game.Graphics.Processing -{ - internal class RatioAdjust : Container - { - public RatioAdjust() - { - RelativeSizeAxes = Axes.Both; - } - - protected override void Update() - { - base.Update(); - Vector2 parent = Parent.DrawSize; - - Scale = new Vector2(Math.Min(parent.Y / 768f, parent.X / 1024f)); - Size = new Vector2(1 / Scale.X); - } - } -} diff --git a/osu.Game/Graphics/SpriteIcon.cs b/osu.Game/Graphics/SpriteIcon.cs index d4f9127d54..326aa2fb79 100644 --- a/osu.Game/Graphics/SpriteIcon.cs +++ b/osu.Game/Graphics/SpriteIcon.cs @@ -15,14 +15,19 @@ namespace osu.Game.Graphics { public class SpriteIcon : CompositeDrawable { - private readonly Sprite spriteShadow; - private readonly Sprite spriteMain; + private Sprite spriteShadow; + private Sprite spriteMain; private Cached layout = new Cached(); - private readonly Container shadowVisibility; + private Container shadowVisibility; - public SpriteIcon() + private FontStore store; + + [BackgroundDependencyLoader] + private void load(FontStore store) { + this.store = store; + InternalChildren = new Drawable[] { shadowVisibility = new Container @@ -39,7 +44,7 @@ namespace osu.Game.Graphics Y = 2, Colour = new Color4(0f, 0f, 0f, 0.2f), }, - Alpha = 0, + Alpha = shadow ? 1 : 0, }, spriteMain = new Sprite { @@ -49,32 +54,32 @@ namespace osu.Game.Graphics FillMode = FillMode.Fit }, }; - } - private FontStore store; - - [BackgroundDependencyLoader] - private void load(FontStore store) - { - this.store = store; + updateTexture(); } protected override void LoadComplete() { base.LoadComplete(); - updateTexture(); } + private FontAwesome loadedIcon; private void updateTexture() { - var texture = store?.Get(((char)icon).ToString()); + var loadableIcon = icon; + + if (loadableIcon == loadedIcon) return; + + var texture = store?.Get(((char)loadableIcon).ToString()); spriteMain.Texture = texture; spriteShadow.Texture = texture; if (Size == Vector2.Zero) Size = new Vector2(texture?.DisplayWidth ?? 0, texture?.DisplayHeight ?? 0); + + loadedIcon = loadableIcon; } public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) @@ -98,12 +103,15 @@ namespace osu.Game.Graphics } } + private bool shadow; public bool Shadow { - get { return spriteShadow.IsPresent; } + get { return shadow; } set { - shadowVisibility.Alpha = value ? 1 : 0; + shadow = value; + if (shadowVisibility != null) + shadowVisibility.Alpha = value ? 1 : 0; } } diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs index f5749846be..cbd9d9582d 100644 --- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs +++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.MathUtils; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Graphics.Transforms; namespace osu.Game.Graphics.Sprites { @@ -40,4 +41,23 @@ namespace osu.Game.Graphics.Sprites return base.CreateFallbackCharacterDrawable(); } } + + public static class OsuSpriteTextTransformExtensions + { + /// + /// Sets to a new value after a duration. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformTextTo(this T spriteText, string newText, double duration = 0, Easing easing = Easing.None) + where T : OsuSpriteText + => spriteText.TransformTo(nameof(OsuSpriteText.Text), newText, duration, easing); + + /// + /// Sets to a new value after a duration. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformTextTo(this TransformSequence t, string newText, double duration = 0, Easing easing = Easing.None) + where T : OsuSpriteText + => t.Append(o => o.TransformTextTo(newText, duration, easing)); + } } diff --git a/osu.Game/Graphics/UserInterface/Bar.cs b/osu.Game/Graphics/UserInterface/Bar.cs index 20df553142..c25a9bf5e9 100644 --- a/osu.Game/Graphics/UserInterface/Bar.cs +++ b/osu.Game/Graphics/UserInterface/Bar.cs @@ -20,6 +20,7 @@ namespace osu.Game.Graphics.UserInterface private const Easing easing = Easing.InOutCubic; private float length; + /// /// Length of the bar, ranges from 0 to 1 /// @@ -134,4 +135,4 @@ namespace osu.Game.Graphics.UserInterface Vertical = TopToBottom | BottomToTop, Horizontal = LeftToRight | RightToLeft, } -} \ No newline at end of file +} diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index 289ccbf5cd..f07bc4114f 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -12,6 +12,8 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.Containers; +using osu.Framework.Configuration; +using osu.Framework.Input; namespace osu.Game.Graphics.UserInterface { @@ -22,62 +24,7 @@ namespace osu.Game.Graphics.UserInterface private const float glow_fade_duration = 250; private const float click_duration = 200; - private Color4 buttonColour; - public Color4 ButtonColour - { - get - { - return buttonColour; - } - set - { - buttonColour = value; - updateGlow(); - colourContainer.Colour = value; - } - } - - private Color4 backgroundColour = OsuColour.Gray(34); - public Color4 BackgroundColour - { - get - { - return backgroundColour; - } - set - { - backgroundColour = value; - background.Colour = value; - } - } - - private string text; - public string Text - { - get - { - return text; - } - set - { - text = value; - spriteText.Text = Text; - } - } - - private float textSize = 28; - internal float TextSize - { - get - { - return textSize; - } - set - { - textSize = value; - spriteText.TextSize = value; - } - } + public readonly BindableBool Selected = new BindableBool(); private readonly Container backgroundContainer; private readonly Container colourContainer; @@ -89,71 +36,6 @@ namespace osu.Game.Graphics.UserInterface private readonly SpriteText spriteText; private Vector2 hoverSpacing => new Vector2(3f, 0f); - private bool didClick; // Used for making sure that the OnMouseDown animation can call instead of OnHoverLost's when clicking - - 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, Easing.In); - flash(); - - this.Delay(click_duration).Schedule(delegate - { - colourContainer.ResizeTo(new Vector2(0.8f, 1f)); - spriteText.Spacing = Vector2.Zero; - glowContainer.FadeOut(); - }); - - return base.OnClick(state); - } - - protected override bool OnHover(Framework.Input.InputState state) - { - spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic); - - colourContainer.ResizeTo(new Vector2(hover_width, 1f), hover_duration, Easing.OutElastic); - glowContainer.FadeIn(glow_fade_duration, Easing.Out); - base.OnHover(state); - return true; - } - - protected override void OnHoverLost(Framework.Input.InputState state) - { - if (!didClick) - { - 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; - } - - private void flash() - { - var flash = new Box - { - RelativeSizeAxes = Axes.Both - }; - - colourContainer.Add(flash); - - flash.Colour = ButtonColour; - flash.Blending = BlendingMode.Additive; - flash.Alpha = 0.3f; - flash.FadeOutFromOne(click_duration); - flash.Expire(); - } - - private void updateGlow() - { - leftGlow.Colour = ColourInfo.GradientHorizontal(new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f), ButtonColour); - centerGlow.Colour = ButtonColour; - rightGlow.Colour = ColourInfo.GradientHorizontal(ButtonColour, new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f)); - } - public DialogButton() { RelativeSizeAxes = Axes.X; @@ -268,6 +150,135 @@ namespace osu.Game.Graphics.UserInterface }; updateGlow(); + + Selected.ValueChanged += selectionChanged; + } + + private Color4 buttonColour; + public Color4 ButtonColour + { + get + { + return buttonColour; + } + set + { + buttonColour = value; + updateGlow(); + colourContainer.Colour = value; + } + } + + private Color4 backgroundColour = OsuColour.Gray(34); + public Color4 BackgroundColour + { + get + { + return backgroundColour; + } + set + { + backgroundColour = value; + background.Colour = value; + } + } + + private string text; + public string Text + { + get + { + return text; + } + set + { + text = value; + spriteText.Text = Text; + } + } + + private float textSize = 28; + public float TextSize + { + get + { + return textSize; + } + set + { + textSize = value; + spriteText.TextSize = value; + } + } + + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => backgroundContainer.ReceiveMouseInputAt(screenSpacePos); + + protected override bool OnClick(InputState state) + { + colourContainer.ResizeTo(new Vector2(1.5f, 1f), click_duration, Easing.In); + flash(); + + this.Delay(click_duration).Schedule(delegate + { + colourContainer.ResizeTo(new Vector2(0.8f, 1f)); + spriteText.Spacing = Vector2.Zero; + glowContainer.FadeOut(); + }); + + return base.OnClick(state); + } + + protected override bool OnHover(InputState state) + { + base.OnHover(state); + + Selected.Value = true; + return true; + } + + protected override void OnHoverLost(InputState state) + { + base.OnHoverLost(state); + Selected.Value = false; + } + + private void selectionChanged(bool isSelected) + { + if (isSelected) + { + spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic); + colourContainer.ResizeTo(new Vector2(hover_width, 1f), hover_duration, Easing.OutElastic); + glowContainer.FadeIn(glow_fade_duration, Easing.Out); + } + else + { + 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); + } + } + + private void flash() + { + var flash = new Box + { + RelativeSizeAxes = Axes.Both + }; + + colourContainer.Add(flash); + + flash.Colour = ButtonColour; + flash.Blending = BlendingMode.Additive; + flash.Alpha = 0.3f; + flash.FadeOutFromOne(click_duration); + flash.Expire(); + } + + private void updateGlow() + { + leftGlow.Colour = ColourInfo.GradientHorizontal(new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f), ButtonColour); + centerGlow.Colour = ButtonColour; + rightGlow.Colour = ColourInfo.GradientHorizontal(ButtonColour, new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f)); } } } diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index fe060f70f0..206c7a839d 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -38,7 +38,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - if (args.Key == Key.Escape) + if (!args.Repeat && args.Key == Key.Escape) { if (Text.Length > 0) Text = string.Empty; diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs new file mode 100644 index 0000000000..0fac1c8092 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs @@ -0,0 +1,36 @@ +// 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.Extensions; +using osu.Framework.Input; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Adds hover and click sounds to a drawable. + /// Does not draw anything. + /// + public class HoverClickSounds : HoverSounds + { + private SampleChannel sampleClick; + + public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) : base(sampleSet) + { + } + + protected override bool OnClick(InputState state) + { + sampleClick?.Play(); + return base.OnClick(state); + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleClick = audio.Sample.Get($@"UI/generic-select{SampleSet.GetDescription()}"); + } + } +} diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs new file mode 100644 index 0000000000..24dbe37567 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -0,0 +1,53 @@ +// 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.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Adds hover sounds to a drawable. + /// Does not draw anything. + /// + public class HoverSounds : CompositeDrawable + { + private SampleChannel sampleHover; + + protected readonly HoverSampleSet SampleSet; + + public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) + { + SampleSet = sampleSet; + RelativeSizeAxes = Axes.Both; + } + + protected override bool OnHover(InputState state) + { + sampleHover?.Play(); + return base.OnHover(state); + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleHover = audio.Sample.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); + } + } + + public enum HoverSampleSet + { + [Description("")] + Loud, + [Description("-soft")] + Normal, + [Description("-softer")] + Soft + } +} diff --git a/osu.Game/Graphics/UserInterface/IconButton.cs b/osu.Game/Graphics/UserInterface/IconButton.cs index 1808dc4b6c..afffd930ef 100644 --- a/osu.Game/Graphics/UserInterface/IconButton.cs +++ b/osu.Game/Graphics/UserInterface/IconButton.cs @@ -15,32 +15,91 @@ namespace osu.Game.Graphics.UserInterface { public class IconButton : OsuClickableContainer { - private readonly SpriteIcon icon; - private readonly Box hover; - private readonly Container content; + private const float button_size = 30; + private Color4? flashColour; + /// + /// The colour that should be flashed when the is clicked. + /// + public Color4 FlashColour + { + get { return flashColour ?? Color4.White; } + set { flashColour = value; } + } + + private Color4? iconColour; + /// + /// The icon colour. This does not affect . + /// + public Color4 IconColour + { + get { return iconColour ?? Color4.White; } + set + { + iconColour = value; + icon.Colour = value; + } + } + + private Color4? iconHoverColour; + /// + /// The icon colour while the is hovered. + /// + public Color4 IconHoverColour + { + get { return iconHoverColour ?? IconColour; } + set { iconHoverColour = value; } + } + + private Color4? hoverColour; + /// + /// The background colour of the while it is hovered. + /// + public Color4 HoverColour + { + get { return hoverColour ?? Color4.White; } + set + { + hoverColour = value; + hover.Colour = value; + } + } + + /// + /// The icon. + /// public FontAwesome Icon { get { return icon.Icon; } set { icon.Icon = value; } } - private const float button_size = 30; - private Color4 flashColour; - + /// + /// The icon scale. This does not affect . + /// public Vector2 IconScale { get { return icon.Scale; } set { icon.Scale = value; } } + /// + /// The size of the while it is not being pressed. + /// + public Vector2 ButtonSize + { + get { return content.Size; } + set { content.Size = value; } + } + + private readonly Container content; + private readonly SpriteIcon icon; + private readonly Box hover; + public IconButton() { AutoSizeAxes = Axes.Both; - Origin = Anchor.Centre; - Anchor = Anchor.Centre; - Children = new Drawable[] { content = new Container @@ -48,7 +107,6 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.Centre, Anchor = Anchor.Centre, Size = new Vector2(button_size), - CornerRadius = 5, Masking = true, EdgeEffect = new EdgeEffectParameters @@ -78,8 +136,11 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(OsuColour colours) { - hover.Colour = colours.Yellow.Opacity(0.6f); - flashColour = colours.Yellow; + if (hoverColour == null) + HoverColour = colours.Yellow.Opacity(0.6f); + + if (flashColour == null) + FlashColour = colours.Yellow; Enabled.ValueChanged += enabled => this.FadeColour(enabled ? Color4.White : colours.Gray9, 200, Easing.OutQuint); } @@ -87,18 +148,20 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(InputState state) { hover.FadeIn(500, Easing.OutQuint); + icon.FadeColour(IconHoverColour, 500, Easing.OutQuint); return base.OnHover(state); } protected override void OnHoverLost(InputState state) { hover.FadeOut(500, Easing.OutQuint); + icon.FadeColour(IconColour, 500, Easing.OutQuint); base.OnHoverLost(state); } protected override bool OnClick(InputState state) { - hover.FlashColour(flashColour, 800, Easing.OutQuint); + hover.FlashColour(FlashColour, 800, Easing.OutQuint); return base.OnClick(state); } diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs index aa9256e576..ea74c67f6c 100644 --- a/osu.Game/Graphics/UserInterface/LineGraph.cs +++ b/osu.Game/Graphics/UserInterface/LineGraph.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Caching; using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -47,7 +48,16 @@ namespace osu.Game.Graphics.UserInterface set { values = value.ToArray(); - applyPath(); + + float max = values.Max(), min = values.Min(); + if (MaxValue > max) max = MaxValue.Value; + if (MinValue < min) min = MinValue.Value; + + ActualMaxValue = max; + ActualMinValue = min; + + pathCached.Invalidate(); + maskingContainer.Width = 0; maskingContainer.ResizeWidthTo(1, transform_duration, Easing.OutQuint); } @@ -63,13 +73,28 @@ namespace osu.Game.Graphics.UserInterface }); } + private bool pending; + public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) { - if ((invalidation & Invalidation.DrawSize) != 0) - applyPath(); + if ((invalidation & Invalidation.DrawSize) > 0) + pathCached.Invalidate(); + return base.Invalidate(invalidation, source, shallPropagate); } + private Cached pathCached = new Cached(); + + protected override void Update() + { + base.Update(); + if (!pathCached.IsValid) + { + applyPath(); + pathCached.Validate(); + } + } + private void applyPath() { path.ClearVertices(); @@ -77,13 +102,6 @@ namespace osu.Game.Graphics.UserInterface 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; diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index ccc23e3ff6..081e59d3a7 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -1,126 +1,18 @@ // 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 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; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface { - public class OsuButton : Button, IFilterable + /// + /// A button with added default sound effects. + /// + public class OsuButton : Button { - private Box hover; - - private SampleChannel sampleClick; - private SampleChannel sampleHover; - - protected Triangles Triangles; - public OsuButton() { - Height = 40; - } - - protected override SpriteText CreateText() => new OsuSpriteText - { - Depth = -1, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Font = @"Exo2.0-Bold", - }; - - public override bool HandleInput => Action != null; - - [BackgroundDependencyLoader] - private void load(OsuColour colours, AudioManager audio) - { - if (Action == null) - Colour = OsuColour.Gray(0.5f); - - BackgroundColour = colours.BlueDark; - - Content.Masking = true; - Content.CornerRadius = 5; - - AddRange(new Drawable[] - { - Triangles = new Triangles - { - RelativeSizeAxes = Axes.Both, - ColourDark = colours.BlueDarker, - ColourLight = colours.Blue, - }, - hover = new Box - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Colour = Color4.White.Opacity(0.1f), - 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); - } - - protected override void OnHoverLost(InputState state) - { - hover.FadeOut(200); - base.OnHoverLost(state); - } - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - Content.ScaleTo(0.9f, 4000, 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); - } - - public IEnumerable FilterTerms => new[] { Text }; - - public bool MatchingFilter - { - set - { - this.FadeTo(value ? 1 : 0); - } + Add(new HoverClickSounds(HoverSampleSet.Loud)); } } } diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 68ff99e593..40ff1243dc 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -70,7 +70,8 @@ namespace osu.Game.Graphics.UserInterface Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding { Right = 5 }, - } + }, + new HoverClickSounds() }; Nub.Current.BindTo(Current); diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index f605804aaa..4401b509fd 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -33,7 +33,6 @@ namespace osu.Game.Graphics.UserInterface if (accentColour == default(Color4)) accentColour = colours.PinkDarker; updateAccentColour(); - } private void updateAccentColour() @@ -137,6 +136,8 @@ namespace osu.Game.Graphics.UserInterface nonAccentHoverColour = colours.PinkDarker; nonAccentSelectedColour = Color4.Black.Opacity(0.5f); updateColours(); + + AddInternal(new HoverClickSounds(HoverSampleSet.Soft)); } protected override void UpdateForegroundColour() @@ -183,7 +184,7 @@ namespace osu.Game.Graphics.UserInterface { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - } + }, }; } } @@ -237,8 +238,10 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.CentreRight, Margin = new MarginPadding { Right = 4 }, Size = new Vector2(20), - } + }, }; + + AddInternal(new HoverClickSounds()); } [BackgroundDependencyLoader] diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index 3fd5481152..7f16d73613 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -72,7 +72,7 @@ namespace osu.Game.Graphics.UserInterface private void load(AudioManager audio) { sampleHover = audio.Sample.Get(@"UI/generic-hover"); - sampleClick = audio.Sample.Get(@"UI/generic-click"); + sampleClick = audio.Sample.Get(@"UI/generic-select"); BackgroundColour = Color4.Transparent; BackgroundColourHover = OsuColour.FromHex(@"172023"); diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 3dd3596c30..fd75269610 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -88,7 +88,8 @@ namespace osu.Game.Graphics.UserInterface { Origin = Anchor.TopCentre, Expanded = true, - } + }, + new HoverClickSounds() }; Current.DisabledChanged += disabled => diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 89b1f4124b..decbf57ad1 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -59,7 +59,7 @@ namespace osu.Game.Graphics.UserInterface public class OsuTabItem : TabItem, IHasAccentColour { protected readonly SpriteText Text; - private readonly Box box; + protected readonly Box Bar; private Color4 accentColour; public Color4 AccentColour @@ -77,13 +77,13 @@ namespace osu.Game.Graphics.UserInterface private void fadeActive() { - box.FadeIn(transition_length, Easing.OutQuint); + Bar.FadeIn(transition_length, Easing.OutQuint); Text.FadeColour(Color4.White, transition_length, Easing.OutQuint); } private void fadeInactive() { - box.FadeOut(transition_length, Easing.OutQuint); + Bar.FadeOut(transition_length, Easing.OutQuint); Text.FadeColour(AccentColour, transition_length, Easing.OutQuint); } @@ -123,7 +123,7 @@ namespace osu.Game.Graphics.UserInterface TextSize = 14, Font = @"Exo2.0-Bold", // Font should only turn bold when active? }, - box = new Box + Bar = new Box { RelativeSizeAxes = Axes.X, Height = 1, @@ -131,7 +131,8 @@ namespace osu.Game.Graphics.UserInterface Colour = Color4.White, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - } + }, + new HoverClickSounds() }; } diff --git a/osu.Game/Graphics/UserInterface/PageTabControl.cs b/osu.Game/Graphics/UserInterface/PageTabControl.cs index 6b97e54ecd..c69857a5c4 100644 --- a/osu.Game/Graphics/UserInterface/PageTabControl.cs +++ b/osu.Game/Graphics/UserInterface/PageTabControl.cs @@ -57,6 +57,7 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, }, + new HoverClickSounds() }; } diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index e581d19d54..b4ca0d2e02 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.MathUtils; using System; +using System.Linq; namespace osu.Game.Graphics.UserInterface { @@ -72,16 +73,9 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(star_spacing), + ChildrenEnumerable = Enumerable.Range(0, StarCount).Select(i => new Star { Alpha = minStarAlpha }) } }; - - for (int i = 0; i < StarCount; i++) - { - stars.Add(new Star - { - Alpha = minStarAlpha, - }); - } } protected override void LoadComplete() @@ -147,15 +141,12 @@ namespace osu.Game.Graphics.UserInterface { Size = new Vector2(star_size); - Children = new[] + Child = Icon = new SpriteIcon { - Icon = new SpriteIcon - { - Size = new Vector2(star_size), - Icon = FontAwesome.fa_star, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } + Size = new Vector2(star_size), + Icon = FontAwesome.fa_star, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }; } } diff --git a/osu.Game/Graphics/UserInterface/TriangleButton.cs b/osu.Game/Graphics/UserInterface/TriangleButton.cs new file mode 100644 index 0000000000..61e9705064 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/TriangleButton.cs @@ -0,0 +1,108 @@ +// 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 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.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// A button with moving triangles in the background. + /// + public class TriangleButton : OsuButton, IFilterable + { + private Box hover; + + protected Triangles Triangles; + + public TriangleButton() + { + Height = 40; + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Font = @"Exo2.0-Bold", + }; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.BlueDark; + + Content.Masking = true; + Content.CornerRadius = 5; + + AddRange(new Drawable[] + { + Triangles = new Triangles + { + RelativeSizeAxes = Axes.Both, + ColourDark = colours.BlueDarker, + ColourLight = colours.Blue, + }, + hover = new Box + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingMode.Additive, + Colour = Color4.White.Opacity(0.1f), + Alpha = 0, + }, + }); + + 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 OnHover(InputState state) + { + hover.FadeIn(200); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + hover.FadeOut(200); + base.OnHoverLost(state); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + Content.ScaleTo(0.9f, 4000, 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); + } + + public IEnumerable FilterTerms => new[] { Text }; + + public bool MatchingFilter + { + set + { + this.FadeTo(value ? 1 : 0); + } + } + } +} diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs index 8c777f491b..273a2279bf 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs @@ -11,7 +11,7 @@ using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface.Volume { - internal class VolumeControl : OverlayContainer + public class VolumeControl : OverlayContainer { private readonly VolumeMeter volumeMeterMaster; @@ -119,4 +119,4 @@ namespace osu.Game.Graphics.UserInterface.Volume 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 c222fecb5d..7c740f16ce 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs @@ -8,7 +8,7 @@ using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface.Volume { - internal class VolumeControlReceptor : Container, IKeyBindingHandler + public class VolumeControlReceptor : Container, IKeyBindingHandler { public Func ActionRequested; diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs index 81c4fa9bae..d55b16563d 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs @@ -13,7 +13,7 @@ using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface.Volume { - internal class VolumeMeter : Container, IKeyBindingHandler + public class VolumeMeter : Container, IKeyBindingHandler { private readonly Box meterFill; public BindableDouble Bindable { get; } = new BindableDouble(); @@ -108,4 +108,4 @@ namespace osu.Game.Graphics.UserInterface.Volume 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 index 367fd68f7b..31b63ecd21 100644 --- a/osu.Game/IO/FileInfo.cs +++ b/osu.Game/IO/FileInfo.cs @@ -1,22 +1,20 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.ComponentModel.DataAnnotations.Schema; using System.IO; -using SQLite.Net.Attributes; namespace osu.Game.IO { public class FileInfo { - [PrimaryKey, AutoIncrement] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 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 index c3d8c1df46..4eca310424 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -9,7 +9,6 @@ using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Database; -using SQLite.Net; namespace osu.Game.IO { @@ -18,75 +17,26 @@ namespace osu.Game.IO /// public class FileStore : DatabaseBackedStore { - private const string prefix = "files"; + public readonly IResourceStore Store; - public readonly ResourceStore Store; + public new Storage Storage => base.Storage; - protected override int StoreVersion => 2; - - public FileStore(SQLiteConnection connection, Storage storage) : base(connection, storage) + public FileStore(Func createContext, Storage storage) : base(createContext, storage.GetStorageForDirectory(@"files")) { - 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; - } - } + Store = new StorageBackedResourceStore(Storage); } public FileInfo Add(Stream data, bool reference = true) { + var context = GetContext(); + string hash = data.ComputeSHA2Hash(); - var existing = Connection.Table().Where(f => f.Hash == hash).FirstOrDefault(); + var existing = context.FileInfo.FirstOrDefault(f => f.Hash == hash); var info = existing ?? new FileInfo { Hash = hash }; - string path = Path.Combine(prefix, info.StoragePath); + string path = info.StoragePath; // we may be re-adding a file to fix missing store entries. if (!Storage.Exists(path)) @@ -99,62 +49,58 @@ namespace osu.Game.IO data.Seek(0, SeekOrigin.Begin); } - if (existing == null) - Connection.Insert(info); - if (reference || existing == null) 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; - }); + public void Reference(params FileInfo[] files) => reference(GetContext(), files); - Connection.UpdateAll(incrementedFiles); - }); + private void reference(OsuDbContext context, FileInfo[] files) + { + foreach (var f in files.GroupBy(f => f.ID)) + { + var refetch = context.Find(f.First().ID) ?? f.First(); + refetch.ReferenceCount += f.Count(); + context.FileInfo.Update(refetch); + } + + context.SaveChanges(); } - 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; - }); + public void Dereference(params FileInfo[] files) => dereference(GetContext(), files); - Connection.UpdateAll(incrementedFiles); - }); + private void dereference(OsuDbContext context, FileInfo[] files) + { + foreach (var f in files.GroupBy(f => f.ID)) + { + var refetch = context.FileInfo.Find(f.Key); + refetch.ReferenceCount -= f.Count(); + context.FileInfo.Update(refetch); + } + + context.SaveChanges(); } - private void deletePending() + public override void Cleanup() { - Connection.RunInTransaction(() => + var context = GetContext(); + + foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1)) { - foreach (var f in Query(f => f.ReferenceCount < 1)) + try { - try - { - Storage.Delete(Path.Combine(prefix, f.StoragePath)); - Connection.Delete(f); - } - catch (Exception e) - { - Logger.Error(e, $@"Could not delete beatmap {f}"); - } + Storage.Delete(f.StoragePath); + context.FileInfo.Remove(f); } - }); + catch (Exception e) + { + Logger.Error(e, $@"Could not delete beatmap {f}"); + } + } + + context.SaveChanges(); } } -} \ No newline at end of file +} diff --git a/osu.Game/IO/Serialization/Converters/TypedListConverter.cs b/osu.Game/IO/Serialization/Converters/TypedListConverter.cs new file mode 100644 index 0000000000..b218f76b53 --- /dev/null +++ b/osu.Game/IO/Serialization/Converters/TypedListConverter.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 Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace osu.Game.IO.Serialization.Converters +{ + /// + /// A type of that serializes a alongside + /// a lookup table for the types contained. The lookup table is used in deserialization to + /// reconstruct the objects with their original types. + /// + /// The type of objects contained in the this attribute is attached to. + public class TypedListConverter : JsonConverter + { + private readonly bool requiresTypeVersion; + + /// + /// Constructs a new . + /// + // ReSharper disable once UnusedMember.Global + public TypedListConverter() + { + } + + /// + /// Constructs a new . + /// + /// Whether the version of the type should be serialized. + // ReSharper disable once UnusedMember.Global (Used in Beatmap) + public TypedListConverter(bool requiresTypeVersion) + { + this.requiresTypeVersion = requiresTypeVersion; + } + + public override bool CanConvert(Type objectType) => objectType == typeof(List); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var list = new List(); + + var obj = JObject.Load(reader); + var lookupTable = serializer.Deserialize>(obj["lookup_table"].CreateReader()); + + foreach (var tok in obj["items"]) + { + var itemReader = tok.CreateReader(); + + var typeName = lookupTable[(int)tok["type"]]; + var instance = (T)Activator.CreateInstance(Type.GetType(typeName)); + serializer.Populate(itemReader, instance); + + list.Add(instance); + } + + return list; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var list = (List)value; + + var lookupTable = new List(); + var objects = new List(); + foreach (var item in list) + { + var type = item.GetType(); + var assemblyName = type.Assembly.GetName(); + + var typeString = $"{type.FullName}, {assemblyName.Name}"; + if (requiresTypeVersion) + typeString += $", {assemblyName.Version}"; + + int typeId = lookupTable.IndexOf(typeString); + if (typeId == -1) + { + lookupTable.Add(typeString); + typeId = lookupTable.Count - 1; + } + + var itemObject = JObject.FromObject(item, serializer); + itemObject.AddFirst(new JProperty("type", typeId)); + objects.Add(itemObject); + } + + writer.WriteStartObject(); + + writer.WritePropertyName("lookup_table"); + serializer.Serialize(writer, lookupTable); + + writer.WritePropertyName("items"); + serializer.Serialize(writer, objects); + + writer.WriteEndObject(); + } + } +} diff --git a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs new file mode 100644 index 0000000000..87fbd31d21 --- /dev/null +++ b/osu.Game/IO/Serialization/Converters/Vector2Converter.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 System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenTK; + +namespace osu.Game.IO.Serialization.Converters +{ + /// + /// A type of that serializes only the X and Y coordinates of a . + /// + public class Vector2Converter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(Vector2); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var obj = JObject.Load(reader); + return new Vector2((float)obj["x"], (float)obj["y"]); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var vector = (Vector2)value; + + writer.WriteStartObject(); + + writer.WritePropertyName("x"); + writer.WriteValue(vector.X); + writer.WritePropertyName("y"); + writer.WriteValue(vector.Y); + + writer.WriteEndObject(); + } + } +} diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs index e725742726..892c266fe3 100644 --- a/osu.Game/IO/Serialization/IJsonSerializable.cs +++ b/osu.Game/IO/Serialization/IJsonSerializable.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using Newtonsoft.Json; +using osu.Game.IO.Serialization.Converters; namespace osu.Game.IO.Serialization { @@ -11,20 +12,26 @@ namespace osu.Game.IO.Serialization public static class JsonSerializableExtensions { - public static string Serialize(this IJsonSerializable obj) - { - return JsonConvert.SerializeObject(obj, new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); - } + public static string Serialize(this IJsonSerializable obj) => JsonConvert.SerializeObject(obj, CreateGlobalSettings()); - public static T Deserialize(this string objString) - { - return JsonConvert.DeserializeObject(objString); - } + public static T Deserialize(this string objString) => JsonConvert.DeserializeObject(objString, CreateGlobalSettings()); - public static T DeepClone(this T obj) - where T : IJsonSerializable + public static void DeserializeInto(this string objString, T target) => JsonConvert.PopulateObject(objString, target, CreateGlobalSettings()); + + public static T DeepClone(this T obj) where T : IJsonSerializable => Deserialize(Serialize(obj)); + + /// + /// Creates the default that should be used for all s. + /// + /// + public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings { - return Deserialize(Serialize(obj)); - } + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + Formatting = Formatting.Indented, + ObjectCreationHandling = ObjectCreationHandling.Replace, + DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, + Converters = new JsonConverter[] { new Vector2Converter() }, + ContractResolver = new KeyContractResolver() + }; } } diff --git a/osu.Game/IO/Serialization/KeyContractResolver.cs b/osu.Game/IO/Serialization/KeyContractResolver.cs new file mode 100644 index 0000000000..10375646ce --- /dev/null +++ b/osu.Game/IO/Serialization/KeyContractResolver.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Humanizer; +using Newtonsoft.Json.Serialization; + +namespace osu.Game.IO.Serialization +{ + public class KeyContractResolver : DefaultContractResolver + { + protected override string ResolvePropertyName(string propertyName) + { + return propertyName.Underscore(); + } + } +} diff --git a/osu.Game/Input/Bindings/DatabasedKeyBinding.cs b/osu.Game/Input/Bindings/DatabasedKeyBinding.cs index cbf74d6984..7e9e25aeff 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBinding.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBinding.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 System.ComponentModel.DataAnnotations.Schema; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets; -using SQLite.Net.Attributes; -using SQLiteNetExtensions.Attributes; +using osu.Game.Database; namespace osu.Game.Input.Bindings { [Table("KeyBinding")] - public class DatabasedKeyBinding : KeyBinding + public class DatabasedKeyBinding : KeyBinding, IHasPrimaryKey { - [PrimaryKey, AutoIncrement] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } - [ForeignKey(typeof(RulesetInfo))] public int? RulesetID { get; set; } - [Indexed] public int? Variant { get; set; } [Column("Keys")] @@ -27,7 +24,6 @@ namespace osu.Game.Input.Bindings private set { KeyCombination = value; } } - [Indexed] [Column("Action")] public int IntAction { @@ -35,4 +31,4 @@ namespace osu.Game.Input.Bindings 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 index 0a4fcf4389..784e6462f2 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingInputManager.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingInputManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Game.Rulesets; +using System.Linq; namespace osu.Game.Input.Bindings { @@ -13,7 +14,7 @@ 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 + public class DatabasedKeyBindingInputManager : KeyBindingContainer where T : struct { private readonly RulesetInfo ruleset; @@ -30,7 +31,7 @@ namespace osu.Game.Input.Bindings /// 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) + public DatabasedKeyBindingInputManager(RulesetInfo ruleset = null, int? variant = null, SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None) : base(simultaneousMode) { this.ruleset = ruleset; @@ -44,11 +45,17 @@ namespace osu.Game.Input.Bindings private void load(KeyBindingStore keyBindings) { store = keyBindings; + store.KeyBindingChanged += ReloadMappings; } - protected override void ReloadMappings() + protected override void Dispose(bool isDisposing) { - KeyBindings = store.Query(ruleset?.ID, variant); + base.Dispose(isDisposing); + + if (store != null) + store.KeyBindingChanged -= ReloadMappings; } + + protected override void ReloadMappings() => KeyBindings = store.Query(ruleset?.ID, variant).ToList(); } -} \ No newline at end of file +} diff --git a/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs b/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs index 759396e195..745508ec9e 100644 --- a/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs +++ b/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs @@ -5,11 +5,12 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using osu.Framework.Graphics; +using osu.Framework.Input; using osu.Framework.Input.Bindings; namespace osu.Game.Input.Bindings { - public class GlobalKeyBindingInputManager : DatabasedKeyBindingInputManager + public class GlobalKeyBindingInputManager : DatabasedKeyBindingInputManager, IHandleGlobalInput { private readonly Drawable handler; diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index c5ba1683dd..9e2988417a 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -9,16 +9,17 @@ 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) + public event Action KeyBindingChanged; + + public KeyBindingStore(Func createContext, RulesetStore rulesets, Storage storage = null) + : base(createContext, storage) { - foreach (var info in rulesets.AllRulesets) + foreach (var info in rulesets.AvailableRulesets) { var ruleset = info.CreateInstance(); foreach (var variant in ruleset.AvailableVariants) @@ -26,68 +27,60 @@ namespace osu.Game.Input } } - 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(); - } + public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { - var query = Query(rulesetId, variant); + var context = GetContext(); - // compare counts in database vs defaults - foreach (var group in defaults.GroupBy(k => k.Action)) + using (var transaction = context.BeginTransaction()) { - int count; - while (group.Count() > (count = query.Count(k => (int)k.Action == (int)group.Key))) + // compare counts in database vs defaults + foreach (var group in defaults.GroupBy(k => k.Action)) { - var insertable = group.Skip(count).First(); + int count = Query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key); + int aimCount = group.Count(); - // insert any defaults which are missing. - Connection.Insert(new DatabasedKeyBinding - { - KeyCombination = insertable.KeyCombination, - Action = insertable.Action, - RulesetID = rulesetId, - Variant = variant - }); + if (aimCount <= count) + continue; + + foreach (var insertable in group.Skip(count).Take(aimCount - count)) + // insert any defaults which are missing. + context.DatabasedKeyBinding.Add(new DatabasedKeyBinding + { + KeyCombination = insertable.KeyCombination, + Action = insertable.Action, + RulesetID = rulesetId, + Variant = variant + }); } + + context.SaveChanges(transaction); } } - protected override Type[] ValidTypes => new[] + /// + /// Retrieve s for a specified ruleset/variant content. + /// + /// The ruleset's internal ID. + /// An optional variant. + /// + public List Query(int? rulesetId = null, int? variant = null) => + GetContext().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + + public void Update(KeyBinding keyBinding) { - typeof(DatabasedKeyBinding) - }; + var dbKeyBinding = (DatabasedKeyBinding)keyBinding; - public IEnumerable Query(int? rulesetId = null, int? variant = null) => - Query(b => b.RulesetID == rulesetId && b.Variant == variant); + var context = GetContext(); - public void Update(KeyBinding keyBinding) => Connection.Update(keyBinding); + Refresh(ref dbKeyBinding); + + dbKeyBinding.KeyCombination = keyBinding.KeyCombination; + + context.SaveChanges(); + + KeyBindingChanged?.Invoke(); + } } } diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs b/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs new file mode 100644 index 0000000000..0820041643 --- /dev/null +++ b/osu.Game/Migrations/20171019041408_InitialCreate.Designer.cs @@ -0,0 +1,293 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20171019041408_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash"); + + b.HasIndex("MD5Hash"); + + b.HasIndex("MetadataID"); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash"); + + b.HasIndex("MetadataID"); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.cs b/osu.Game/Migrations/20171019041408_InitialCreate.cs new file mode 100644 index 0000000000..23e5b6f8bb --- /dev/null +++ b/osu.Game/Migrations/20171019041408_InitialCreate.cs @@ -0,0 +1,313 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace osu.Game.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "BeatmapDifficulty", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + ApproachRate = table.Column(type: "REAL", nullable: false), + CircleSize = table.Column(type: "REAL", nullable: false), + DrainRate = table.Column(type: "REAL", nullable: false), + OverallDifficulty = table.Column(type: "REAL", nullable: false), + SliderMultiplier = table.Column(type: "REAL", nullable: false), + SliderTickRate = table.Column(type: "REAL", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapDifficulty", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "BeatmapMetadata", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Artist = table.Column(type: "TEXT", nullable: true), + ArtistUnicode = table.Column(type: "TEXT", nullable: true), + AudioFile = table.Column(type: "TEXT", nullable: true), + Author = table.Column(type: "TEXT", nullable: true), + BackgroundFile = table.Column(type: "TEXT", nullable: true), + PreviewTime = table.Column(type: "INTEGER", nullable: false), + Source = table.Column(type: "TEXT", nullable: true), + Tags = table.Column(type: "TEXT", nullable: true), + Title = table.Column(type: "TEXT", nullable: true), + TitleUnicode = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapMetadata", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "FileInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Hash = table.Column(type: "TEXT", nullable: true), + ReferenceCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FileInfo", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "KeyBinding", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Action = table.Column(type: "INTEGER", nullable: false), + Keys = table.Column(type: "TEXT", nullable: true), + RulesetID = table.Column(type: "INTEGER", nullable: true), + Variant = table.Column(type: "INTEGER", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_KeyBinding", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "RulesetInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Available = table.Column(type: "INTEGER", nullable: false), + InstantiationInfo = table.Column(type: "TEXT", nullable: true), + Name = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RulesetInfo", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "BeatmapSetInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + DeletePending = table.Column(type: "INTEGER", nullable: false), + Hash = table.Column(type: "TEXT", nullable: true), + MetadataID = table.Column(type: "INTEGER", nullable: true), + OnlineBeatmapSetID = table.Column(type: "INTEGER", nullable: true), + Protected = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapSetInfo", x => x.ID); + table.ForeignKey( + name: "FK_BeatmapSetInfo_BeatmapMetadata_MetadataID", + column: x => x.MetadataID, + principalTable: "BeatmapMetadata", + principalColumn: "ID", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "BeatmapInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + AudioLeadIn = table.Column(type: "INTEGER", nullable: false), + BaseDifficultyID = table.Column(type: "INTEGER", nullable: false), + BeatDivisor = table.Column(type: "INTEGER", nullable: false), + BeatmapSetInfoID = table.Column(type: "INTEGER", nullable: false), + Countdown = table.Column(type: "INTEGER", nullable: false), + DistanceSpacing = table.Column(type: "REAL", nullable: false), + GridSize = table.Column(type: "INTEGER", nullable: false), + Hash = table.Column(type: "TEXT", nullable: true), + Hidden = table.Column(type: "INTEGER", nullable: false), + LetterboxInBreaks = table.Column(type: "INTEGER", nullable: false), + MD5Hash = table.Column(type: "TEXT", nullable: true), + MetadataID = table.Column(type: "INTEGER", nullable: true), + OnlineBeatmapID = table.Column(type: "INTEGER", nullable: true), + Path = table.Column(type: "TEXT", nullable: true), + RulesetID = table.Column(type: "INTEGER", nullable: false), + SpecialStyle = table.Column(type: "INTEGER", nullable: false), + StackLeniency = table.Column(type: "REAL", nullable: false), + StarDifficulty = table.Column(type: "REAL", nullable: false), + StoredBookmarks = table.Column(type: "TEXT", nullable: true), + TimelineZoom = table.Column(type: "REAL", nullable: false), + Version = table.Column(type: "TEXT", nullable: true), + WidescreenStoryboard = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapInfo", x => x.ID); + table.ForeignKey( + name: "FK_BeatmapInfo_BeatmapDifficulty_BaseDifficultyID", + column: x => x.BaseDifficultyID, + principalTable: "BeatmapDifficulty", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_BeatmapInfo_BeatmapSetInfo_BeatmapSetInfoID", + column: x => x.BeatmapSetInfoID, + principalTable: "BeatmapSetInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_BeatmapInfo_BeatmapMetadata_MetadataID", + column: x => x.MetadataID, + principalTable: "BeatmapMetadata", + principalColumn: "ID", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_BeatmapInfo_RulesetInfo_RulesetID", + column: x => x.RulesetID, + principalTable: "RulesetInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "BeatmapSetFileInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + BeatmapSetInfoID = table.Column(type: "INTEGER", nullable: false), + FileInfoID = table.Column(type: "INTEGER", nullable: false), + Filename = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BeatmapSetFileInfo", x => x.ID); + table.ForeignKey( + name: "FK_BeatmapSetFileInfo_BeatmapSetInfo_BeatmapSetInfoID", + column: x => x.BeatmapSetInfoID, + principalTable: "BeatmapSetInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_BeatmapSetFileInfo_FileInfo_FileInfoID", + column: x => x.FileInfoID, + principalTable: "FileInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_BaseDifficultyID", + table: "BeatmapInfo", + column: "BaseDifficultyID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_BeatmapSetInfoID", + table: "BeatmapInfo", + column: "BeatmapSetInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MetadataID", + table: "BeatmapInfo", + column: "MetadataID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_RulesetID", + table: "BeatmapInfo", + column: "RulesetID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetFileInfo_BeatmapSetInfoID", + table: "BeatmapSetFileInfo", + column: "BeatmapSetInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetFileInfo_FileInfoID", + table: "BeatmapSetFileInfo", + column: "FileInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_DeletePending", + table: "BeatmapSetInfo", + column: "DeletePending"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_MetadataID", + table: "BeatmapSetInfo", + column: "MetadataID"); + + migrationBuilder.CreateIndex( + name: "IX_FileInfo_Hash", + table: "FileInfo", + column: "Hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FileInfo_ReferenceCount", + table: "FileInfo", + column: "ReferenceCount"); + + migrationBuilder.CreateIndex( + name: "IX_KeyBinding_Action", + table: "KeyBinding", + column: "Action"); + + migrationBuilder.CreateIndex( + name: "IX_KeyBinding_Variant", + table: "KeyBinding", + column: "Variant"); + + migrationBuilder.CreateIndex( + name: "IX_RulesetInfo_Available", + table: "RulesetInfo", + column: "Available"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BeatmapInfo"); + + migrationBuilder.DropTable( + name: "BeatmapSetFileInfo"); + + migrationBuilder.DropTable( + name: "KeyBinding"); + + migrationBuilder.DropTable( + name: "BeatmapDifficulty"); + + migrationBuilder.DropTable( + name: "RulesetInfo"); + + migrationBuilder.DropTable( + name: "BeatmapSetInfo"); + + migrationBuilder.DropTable( + name: "FileInfo"); + + migrationBuilder.DropTable( + name: "BeatmapMetadata"); + } + } +} diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs new file mode 100644 index 0000000000..478f27e82c --- /dev/null +++ b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.Designer.cs @@ -0,0 +1,299 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20171025071459_AddMissingIndexRules")] + partial class AddMissingIndexRules + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs new file mode 100644 index 0000000000..a20652eedc --- /dev/null +++ b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs @@ -0,0 +1,82 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace osu.Game.Migrations +{ + public partial class AddMissingIndexRules : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo", + column: "Hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_OnlineBeatmapSetID", + table: "BeatmapSetInfo", + column: "OnlineBeatmapSetID", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapSetInfo_OnlineBeatmapSetID", + table: "BeatmapSetInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapSetInfo_Hash", + table: "BeatmapSetInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash"); + } + } +} diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs new file mode 100644 index 0000000000..b2f81a729a --- /dev/null +++ b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.Designer.cs @@ -0,0 +1,302 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20171119065731_AddBeatmapOnlineIDUniqueConstraint")] + partial class AddBeatmapOnlineIDUniqueConstraint + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs new file mode 100644 index 0000000000..d3830ec0f5 --- /dev/null +++ b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace osu.Game.Migrations +{ + public partial class AddBeatmapOnlineIDUniqueConstraint : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_OnlineBeatmapID", + table: "BeatmapInfo", + column: "OnlineBeatmapID", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_OnlineBeatmapID", + table: "BeatmapInfo"); + } + } +} diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs new file mode 100644 index 0000000000..6023e88994 --- /dev/null +++ b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.Designer.cs @@ -0,0 +1,307 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20171209034410_AddRulesetInfoShortName")] + partial class AddRulesetInfoShortName + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs new file mode 100644 index 0000000000..94519c0b97 --- /dev/null +++ b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace osu.Game.Migrations +{ + public partial class AddRulesetInfoShortName : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ShortName", + table: "RulesetInfo", + type: "TEXT", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_RulesetInfo_ShortName", + table: "RulesetInfo", + column: "ShortName", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_RulesetInfo_ShortName", + table: "RulesetInfo"); + + migrationBuilder.DropColumn( + name: "ShortName", + table: "RulesetInfo"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs new file mode 100644 index 0000000000..cd4d3c2854 --- /dev/null +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -0,0 +1,306 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + partial class OsuDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 57f5c54a18..daf56657d2 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -101,18 +101,16 @@ namespace osu.Game.Online.API } break; case APIState.Offline: + case APIState.Connecting: //work to restore a connection... if (!HasLogin) { - //OsuGame.Scheduler.Add(() => { OsuGame.ShowLogin(); }); - State = APIState.Offline; - Thread.Sleep(500); + Thread.Sleep(50); continue; } - if (State < APIState.Connecting) - State = APIState.Connecting; + State = APIState.Connecting; if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(Username, Password)) { @@ -120,29 +118,39 @@ namespace osu.Game.Online.API //NotificationOverlay.ShowMessage("Login failed!"); log.Add(@"Login failed!"); Password = null; + authentication.Clear(); continue; } - var userReq = new GetUserRequest(); - userReq.Success += u => { + userReq.Success += u => + { LocalUser.Value = u; + failureCount = 0; + //we're connected! State = APIState.Online; - failureCount = 0; }; if (!handleRequest(userReq)) { - State = APIState.Failing; + Thread.Sleep(500); continue; } + + // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. + // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests + // before actually going online. + while (State != APIState.Online) + Thread.Sleep(500); + break; } //hard bail if we can't get a valid access token. if (authentication.RequestAccessToken() == null) { + Logout(false); State = APIState.Offline; continue; } @@ -162,20 +170,12 @@ namespace osu.Game.Online.API } } - private void clearCredentials() - { - Username = null; - Password = null; - } - public void Login(string username, string password) { Debug.Assert(State == APIState.Offline); Username = username; Password = password; - - State = APIState.Connecting; } /// @@ -204,7 +204,7 @@ namespace osu.Game.Online.API switch (statusCode) { case HttpStatusCode.Unauthorized: - State = APIState.Offline; + Logout(false); return true; case HttpStatusCode.RequestTimeout: failureCount++; @@ -215,6 +215,7 @@ namespace osu.Game.Online.API return false; State = APIState.Failing; + flushQueue(); return true; } @@ -235,33 +236,21 @@ namespace osu.Game.Online.API public APIState State { get { return state; } - set + private set { APIState oldState = state; APIState newState = value; state = value; - switch (state) - { - case APIState.Failing: - case APIState.Offline: - flushQueue(); - break; - } - if (oldState != newState) { - //OsuGame.Scheduler.Add(delegate + log.Add($@"We just went {newState}!"); + Scheduler.Add(delegate { - //NotificationOverlay.ShowMessage($@"We just went {newState}!", newState == APIState.Online ? Color4.YellowGreen : Color4.OrangeRed, 5000); - log.Add($@"We just went {newState}!"); - Scheduler.Add(delegate - { - components.ForEach(c => c.APIStateChanged(this, newState)); - OnStateChange?.Invoke(oldState, newState); - }); - } + components.ForEach(c => c.APIStateChanged(this, newState)); + OnStateChange?.Invoke(oldState, newState); + }); } } } @@ -292,11 +281,12 @@ namespace osu.Game.Online.API } } - public void Logout() + public void Logout(bool clearUsername = true) { - clearCredentials(); + flushQueue(); + if (clearUsername) Username = null; + Password = null; authentication.Clear(); - State = APIState.Offline; LocalUser.Value = createGuestUser(); } diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 37903f924f..71b18103b0 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -36,7 +36,7 @@ namespace osu.Game.Online.API return request; } - private void request_Progress(WebRequest request, long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); }); + private void request_Progress(long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); }); protected APIDownloadRequest() { @@ -99,14 +99,14 @@ namespace osu.Game.Online.API throw new TimeoutException(@"API request timeout hit"); WebRequest = CreateWebRequest(); - WebRequest.RetryCount = 0; - WebRequest.Headers[@"Authorization"] = $@"Bearer {api.AccessToken}"; + WebRequest.AllowRetryOnTimeout = false; + WebRequest.AddHeader("Authorization", $"Bearer {api.AccessToken}"); if (checkAndProcessFailure()) return; if (!WebRequest.Aborted) //could have been aborted by a Cancel() call - WebRequest.BlockingPerform(); + WebRequest.Perform(); if (checkAndProcessFailure()) return; diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 5410bcc55d..322688ced9 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -6,7 +6,7 @@ using osu.Framework.IO.Network; namespace osu.Game.Online.API { - internal class OAuth + public class OAuth { private readonly string clientId; private readonly string clientSecret; @@ -27,6 +27,9 @@ namespace osu.Game.Online.API internal bool AuthenticateWithLogin(string username, string password) { + if (string.IsNullOrEmpty(username)) return false; + if (string.IsNullOrEmpty(password)) return false; + using (var req = new AccessTokenRequestPassword(username, password) { Url = $@"{endpoint}/oauth/token", @@ -37,7 +40,7 @@ namespace osu.Game.Online.API { try { - req.BlockingPerform(); + req.Perform(); } catch { @@ -61,7 +64,7 @@ namespace osu.Game.Online.API ClientSecret = clientSecret }) { - req.BlockingPerform(); + req.Perform(); Token = req.ResponseObject; return true; @@ -127,7 +130,8 @@ namespace osu.Game.Online.API protected override void PrePerform() { - Parameters[@"refresh_token"] = RefreshToken; + AddParameter("refresh_token", RefreshToken); + base.PrePerform(); } } @@ -146,8 +150,9 @@ namespace osu.Game.Online.API protected override void PrePerform() { - Parameters[@"username"] = Username; - Parameters[@"password"] = Password; + AddParameter("username", Username); + AddParameter("password", Password); + base.PrePerform(); } } @@ -161,9 +166,10 @@ namespace osu.Game.Online.API protected override void PrePerform() { - Parameters[@"grant_type"] = GrantType; - Parameters[@"client_id"] = ClientId; - Parameters[@"client_secret"] = ClientSecret; + AddParameter("grant_type", GrantType); + AddParameter("client_id", ClientId); + AddParameter("client_secret", ClientSecret); + base.PrePerform(); } } diff --git a/osu.Game/Online/API/OAuthToken.cs b/osu.Game/Online/API/OAuthToken.cs index 2abd7b6c1f..0c9dc26b59 100644 --- a/osu.Game/Online/API/OAuthToken.cs +++ b/osu.Game/Online/API/OAuthToken.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json; namespace osu.Game.Online.API { [Serializable] - internal class OAuthToken + public class OAuthToken { /// /// OAuth 2.0 access token. diff --git a/osu.Game/Online/API/Requests/APIResponseBeatmapSet.cs b/osu.Game/Online/API/Requests/APIResponseBeatmapSet.cs new file mode 100644 index 0000000000..1b30853421 --- /dev/null +++ b/osu.Game/Online/API/Requests/APIResponseBeatmapSet.cs @@ -0,0 +1,141 @@ +// 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.Rulesets; +using System; + +namespace osu.Game.Online.API.Requests +{ + public class APIResponseBeatmapSet : BeatmapMetadata // todo: this is a bit wrong... + { + [JsonProperty(@"covers")] + private BeatmapSetOnlineCovers covers { get; set; } + + [JsonProperty(@"preview_url")] + private string preview { get; set; } + + [JsonProperty(@"play_count")] + private int playCount { get; set; } + + [JsonProperty(@"favourite_count")] + private int favouriteCount { get; set; } + + [JsonProperty(@"bpm")] + private double bpm { get; set; } + + [JsonProperty(@"video")] + private bool hasVideo { get; set; } + + [JsonProperty(@"submitted_date")] + private DateTimeOffset submitted { get; set; } + + [JsonProperty(@"ranked_date")] + private DateTimeOffset ranked { get; set; } + + [JsonProperty(@"last_updated")] + private DateTimeOffset lastUpdated { get; set; } + + [JsonProperty(@"user_id")] + private long creatorId { + set { Author.Id = value; } + } + + [JsonProperty(@"beatmaps")] + private IEnumerable beatmaps { get; set; } + + public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) + { + return new BeatmapSetInfo + { + OnlineBeatmapSetID = OnlineBeatmapSetID, + Metadata = this, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = covers, + Preview = preview, + PlayCount = playCount, + FavouriteCount = favouriteCount, + BPM = bpm, + HasVideo = hasVideo, + Submitted = submitted, + Ranked = ranked, + LastUpdated = lastUpdated, + }, + Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(), + }; + } + + private class APIResponseBeatmap : BeatmapMetadata + { + [JsonProperty(@"id")] + private int onlineBeatmapID { get; set; } + + [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; } + + [JsonProperty(@"drain")] + private float drainRate { get; set; } + + [JsonProperty(@"cs")] + private float circleSize { get; set; } + + [JsonProperty(@"ar")] + private float approachRate { get; set; } + + [JsonProperty(@"accuracy")] + private float overallDifficulty { get; set; } + + [JsonProperty(@"total_length")] + private double length { get; set; } + + [JsonProperty(@"count_circles")] + private int circleCount { get; set; } + + [JsonProperty(@"count_sliders")] + private int sliderCount { get; set; } + + [JsonProperty(@"version")] + private string version { get; set; } + + public BeatmapInfo ToBeatmap(RulesetStore rulesets) + { + return new BeatmapInfo + { + Metadata = this, + Ruleset = rulesets.GetRuleset(ruleset), + StarDifficulty = starDifficulty, + OnlineBeatmapID = onlineBeatmapID, + Version = version, + BaseDifficulty = new BeatmapDifficulty + { + DrainRate = drainRate, + CircleSize = circleSize, + ApproachRate = approachRate, + OverallDifficulty = overallDifficulty, + }, + OnlineInfo = new BeatmapOnlineInfo + { + PlayCount = playCount, + PassCount = passCount, + Length = length, + CircleCount = circleCount, + SliderCount = sliderCount, + }, + }; + } + } + } +} diff --git a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs index 5a9f609bca..cdcc06a65c 100644 --- a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs @@ -12,13 +12,16 @@ namespace osu.Game.Online.API.Requests public Action DownloadProgressed; - public DownloadBeatmapSetRequest(BeatmapSetInfo set) + private readonly bool noVideo; + + public DownloadBeatmapSetRequest(BeatmapSetInfo set, bool noVideo) { + this.noVideo = noVideo; BeatmapSet = set; Progress += (current, total) => DownloadProgressed?.Invoke((float) current / total); } - protected override string Target => $@"beatmapsets/{BeatmapSet.OnlineBeatmapSetID}/download"; + protected override string Target => $@"beatmapsets/{BeatmapSet.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}"; } } diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs new file mode 100644 index 0000000000..1e6ceaafc6 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.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.Online.API.Requests +{ + public class GetBeatmapSetRequest : APIRequest + { + private readonly int beatmapSetId; + + public GetBeatmapSetRequest(int beatmapSetId) + { + this.beatmapSetId = beatmapSetId; + } + + protected override string Target => $@"beatmapsets/{beatmapSetId}"; + } +} diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.cs deleted file mode 100644 index 470e13ea7b..0000000000 --- a/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.cs +++ /dev/null @@ -1,114 +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 Newtonsoft.Json; -using osu.Game.Beatmaps; -using osu.Game.Overlays; -using osu.Game.Overlays.Direct; -using osu.Game.Rulesets; -using osu.Game.Users; - -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(@"id")] - private int onlineId { get; set; } - - [JsonProperty(@"creator")] - private string creatorUsername; - - [JsonProperty(@"user_id")] - private long creatorId = 1; - - [JsonProperty(@"beatmaps")] - private IEnumerable beatmaps { get; set; } - - public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) - { - return new BeatmapSetInfo - { - OnlineBeatmapSetID = onlineId, - Metadata = this, - OnlineInfo = new BeatmapSetOnlineInfo - { - Author = new User - { - Id = creatorId, - Username = creatorUsername, - }, - 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 13bd8d288d..065c770738 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -6,22 +6,32 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Users; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Select.Leaderboards; +using osu.Framework.IO.Network; namespace osu.Game.Online.API.Requests { public class GetScoresRequest : APIRequest { private readonly BeatmapInfo beatmap; + private readonly LeaderboardScope scope; + private readonly RulesetInfo ruleset; - public GetScoresRequest(BeatmapInfo beatmap) + public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, LeaderboardScope scope = LeaderboardScope.Global) { if (!beatmap.OnlineBeatmapID.HasValue) throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}."); + if (scope == LeaderboardScope.Local) + throw new InvalidOperationException("Should not attempt to request online scores for a local scoped leaderboard"); + this.beatmap = beatmap; + this.scope = scope; + this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset)); Success += onSuccess; } @@ -32,6 +42,17 @@ namespace osu.Game.Online.API.Requests score.ApplyBeatmap(beatmap); } + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.Timeout = 30000; + req.AddParameter(@"type", scope.ToString().ToLowerInvariant()); + req.AddParameter(@"mode", ruleset.ShortName); + + return req; + } + protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores"; } @@ -67,6 +88,9 @@ namespace osu.Game.Online.API.Requests set { Replay = value; } } + [JsonProperty(@"mode_int")] + public int OnlineRulesetID { get; set; } + [JsonProperty(@"score_id")] private long onlineScoreID { @@ -79,8 +103,20 @@ namespace osu.Game.Online.API.Requests set { Date = value; } } + [JsonProperty(@"beatmap")] + private BeatmapInfo beatmap + { + set { Beatmap = value; } + } + + [JsonProperty(@"beatmapset")] + private BeatmapMetadata metadata + { + set { Beatmap.Metadata = value; } + } + [JsonProperty(@"statistics")] - private Dictionary jsonStats + private Dictionary jsonStats { set { @@ -116,7 +152,12 @@ namespace osu.Game.Online.API.Requests public void ApplyBeatmap(BeatmapInfo beatmap) { Beatmap = beatmap; - Ruleset = beatmap.Ruleset; + ApplyRuleset(beatmap.Ruleset); + } + + public void ApplyRuleset(RulesetInfo ruleset) + { + Ruleset = ruleset; // Evaluate the mod string Mods = Ruleset.CreateInstance().GetAllMods().Where(mod => modStrings.Contains(mod.ShortenedName)).ToArray(); diff --git a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs new file mode 100644 index 0000000000..173562e04d --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Humanizer; +using System.Collections.Generic; + +namespace osu.Game.Online.API.Requests +{ + public class GetUserBeatmapsRequest : APIRequest> + { + private readonly long userId; + private readonly int offset; + private readonly BeatmapSetType type; + + public GetUserBeatmapsRequest(long userId, BeatmapSetType type, int offset = 0) + { + this.userId = userId; + this.offset = offset; + this.type = type; + } + + protected override string Target => $@"users/{userId}/beatmapsets/{type.ToString().Underscore()}?offset={offset}"; + } + + public enum BeatmapSetType + { + Favourite, + RankedAndApproved, + Unranked, + Graveyard + } +} diff --git a/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs new file mode 100644 index 0000000000..431a14085f --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using System.Collections.Generic; + +namespace osu.Game.Online.API.Requests +{ + public class GetUserMostPlayedBeatmapsRequest : APIRequest> + { + private readonly long userId; + private readonly int offset; + + public GetUserMostPlayedBeatmapsRequest(long userId, int offset = 0) + { + this.userId = userId; + this.offset = offset; + } + + protected override string Target => $@"users/{userId}/beatmapsets/most_played?offset={offset}"; + } + + public class MostPlayedBeatmap + { + [JsonProperty("beatmap_id")] + public int BeatmapID; + + [JsonProperty("count")] + public int PlayCount; + + [JsonProperty] + private BeatmapInfo beatmap; + + [JsonProperty] + private APIResponseBeatmapSet beatmapSet; + + public BeatmapInfo GetBeatmapInfo(RulesetStore rulesets) + { + BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets); + beatmap.BeatmapSet = setInfo; + beatmap.OnlineBeatmapSetID = setInfo.OnlineBeatmapSetID; + beatmap.Metadata = setInfo.Metadata; + return beatmap; + } + } +} diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs new file mode 100644 index 0000000000..98db234196 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Game.Online.API.Requests +{ + public class GetUserScoresRequest : APIRequest> + { + private readonly long userId; + private readonly ScoreType type; + private readonly int offset; + + public GetUserScoresRequest(long userId, ScoreType type, int offset = 0) + { + this.userId = userId; + this.type = type; + this.offset = offset; + } + + protected override string Target => $@"users/{userId}/scores/{type.ToString().ToLower()}?offset={offset}"; + } + + public enum ScoreType + { + Best, + Firsts, + Recent + } +} \ No newline at end of file diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs new file mode 100644 index 0000000000..4e6c70124f --- /dev/null +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Overlays.Direct; +using osu.Game.Rulesets; + +namespace osu.Game.Online.API.Requests +{ + public class SearchBeatmapSetsRequest : 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 SearchBeatmapSetsRequest(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}"; + } +} diff --git a/osu.Game/Online/Multiplayer/GameType.cs b/osu.Game/Online/Multiplayer/GameType.cs index c94b409d1b..7bec0e94bb 100644 --- a/osu.Game/Online/Multiplayer/GameType.cs +++ b/osu.Game/Online/Multiplayer/GameType.cs @@ -100,7 +100,7 @@ namespace osu.Game.Online.Multiplayer } } - internal class VersusRow : FillFlowContainer + public class VersusRow : FillFlowContainer { public VersusRow(Color4 first, Color4 second, float size) { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c137b8f6f5..4745733bd9 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -109,7 +109,7 @@ namespace osu.Game dependencies.Cache(this); configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); - Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value); + Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First(); Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0; } @@ -157,57 +157,66 @@ namespace osu.Game BeatmapManager.PostNotification = n => notificationOverlay?.Post(n); BeatmapManager.GetStableStorage = GetStorageForStableInstall; - AddRange(new Drawable[] { + AddRange(new Drawable[] + { new VolumeControlReceptor { RelativeSizeAxes = Axes.Both, ActionRequested = action => volume.Adjust(action) }, - mainContent = new Container - { - RelativeSizeAxes = Axes.Both, - }, - volume = new VolumeControl(), - overlayContent = new Container { RelativeSizeAxes = Axes.Both }, - new OnScreenDisplay(), + mainContent = new Container { RelativeSizeAxes = Axes.Both }, + overlayContent = new Container { RelativeSizeAxes = Axes.Both, Depth = float.MinValue }, }); - LoadComponentAsync(screenStack = new Loader(), d => + loadComponentSingleFile(screenStack = new Loader(), d => { screenStack.ModePushed += screenAdded; screenStack.Exited += screenRemoved; mainContent.Add(screenStack); }); + loadComponentSingleFile(Toolbar = new Toolbar + { + Depth = -5, + OnHome = delegate + { + hideAllOverlays(); + intro?.ChildScreen?.MakeCurrent(); + }, + }, overlayContent.Add); + + loadComponentSingleFile(volume = new VolumeControl(), Add); + loadComponentSingleFile(new OnScreenDisplay(), Add); + //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 MainSettings + loadComponentSingleFile(direct = new DirectOverlay { Depth = -1 }, mainContent.Add); + loadComponentSingleFile(social = new SocialOverlay { Depth = -1 }, mainContent.Add); + loadComponentSingleFile(chat = new ChatOverlay { Depth = -1 }, mainContent.Add); + loadComponentSingleFile(settings = new MainSettings { GetToolbarHeight = () => ToolbarOffset, Depth = -1 }, overlayContent.Add); - LoadComponentAsync(userProfile = new UserProfileOverlay { Depth = -2 }, mainContent.Add); - LoadComponentAsync(beatmapSetOverlay = new BeatmapSetOverlay { Depth = -2 }, mainContent.Add); - LoadComponentAsync(musicController = new MusicController + loadComponentSingleFile(userProfile = new UserProfileOverlay { Depth = -2 }, mainContent.Add); + loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay { Depth = -3 }, mainContent.Add); + loadComponentSingleFile(musicController = new MusicController { - Depth = -3, + Depth = -4, Position = new Vector2(0, Toolbar.HEIGHT), Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }, overlayContent.Add); - LoadComponentAsync(notificationOverlay = new NotificationOverlay + loadComponentSingleFile(notificationOverlay = new NotificationOverlay { - Depth = -3, + Depth = -4, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, }, overlayContent.Add); - LoadComponentAsync(dialogOverlay = new DialogOverlay + loadComponentSingleFile(dialogOverlay = new DialogOverlay { - Depth = -5, + Depth = -6, }, overlayContent.Add); Logger.NewEntry += entry => @@ -246,15 +255,21 @@ namespace osu.Game }; } - LoadComponentAsync(Toolbar = new Toolbar + // eventually informational overlays should be displayed in a stack, but for now let's only allow one to stay open at a time. + var informationalOverlays = new OverlayContainer[] { beatmapSetOverlay, userProfile }; + foreach (var overlay in informationalOverlays) { - Depth = -4, - OnHome = delegate + overlay.StateChanged += state => { - hideAllOverlays(); - intro?.ChildScreen?.MakeCurrent(); - }, - }, overlayContent.Add); + if (state == Visibility.Hidden) return; + + foreach (var c in informationalOverlays) + { + if (c == overlay) continue; + c.State = Visibility.Hidden; + } + }; + } settings.StateChanged += delegate { @@ -272,6 +287,17 @@ namespace osu.Game Cursor.State = Visibility.Hidden; } + private Task asyncLoadStream; + + private void loadComponentSingleFile(T d, Action add) + where T : Drawable + { + // schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached). + // with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile, + // we could avoid the need for scheduling altogether. + Schedule(() => { asyncLoadStream = asyncLoadStream?.ContinueWith(t => LoadComponentAsync(d, add).Wait()) ?? LoadComponentAsync(d, add); }); + } + public bool OnPressed(GlobalAction action) { if (intro == null) return false; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8e7bfa8a76..0ddff5e5aa 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Reflection; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Configuration; using osu.Framework.Development; using osu.Framework.Graphics; @@ -15,10 +16,9 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; 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.Framework.Logging; using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; @@ -82,23 +82,17 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); - private SQLiteConnection createConnection() - { - var conn = Host.Storage.GetDatabase(@"client"); - conn.BusyTimeout = new TimeSpan(TimeSpan.TicksPerSecond * 10); - return conn; - } - - private SQLiteConnection connection; + private DatabaseContextFactory contextFactory; [BackgroundDependencyLoader] private void load() { + dependencies.Cache(contextFactory = new DatabaseContextFactory(Host)); + dependencies.Cache(this); dependencies.Cache(LocalConfig); - connection = createConnection(); - connection.CreateTable(); + runMigrations(); dependencies.Cache(API = new APIAccess { @@ -106,11 +100,11 @@ namespace osu.Game Token = LocalConfig.Get(OsuSetting.Token) }); - dependencies.Cache(RulesetStore = new RulesetStore(connection)); - dependencies.Cache(FileStore = new FileStore(connection, Host.Storage)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, FileStore, connection, RulesetStore, API, Host)); - dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, connection, Host, BeatmapManager, RulesetStore)); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(connection, RulesetStore)); + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory.GetContext)); + dependencies.Cache(FileStore = new FileStore(contextFactory.GetContext, Host.Storage)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory.GetContext, RulesetStore, API, Host)); + dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory.GetContext, Host, BeatmapManager, RulesetStore)); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory.GetContext, RulesetStore)); dependencies.Cache(new OsuColour()); //this completely overrides the framework default. will need to change once we make a proper FontStore. @@ -144,6 +138,10 @@ namespace osu.Game Beatmap = new NonNullableBindable(defaultBeatmap); BeatmapManager.DefaultBeatmap = defaultBeatmap; + // tracks play so loud our samples can't keep up. + // this adds a global reduction of track volume for the time being. + Audio.Track.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8)); + Beatmap.ValueChanged += b => { var trackLoaded = lastBeatmap?.TrackLoaded ?? false; @@ -156,7 +154,7 @@ namespace osu.Game Debug.Assert(lastBeatmap != null); Debug.Assert(lastBeatmap.Track != null); - lastBeatmap.DisposeTrack(); + lastBeatmap.RecycleTrack(); } Audio.Track.AddItem(b.Track); @@ -166,6 +164,30 @@ namespace osu.Game }; API.Register(this); + + FileStore.Cleanup(); + } + + private void runMigrations() + { + try + { + using (var context = contextFactory.GetContext()) + context.Migrate(); + } + catch (MigrationFailedException e) + { + Logger.Log((e.InnerException ?? e).ToString(), LoggingTarget.Database, LogLevel.Error); + Logger.Log("Migration failed! We'll be starting with a fresh database.", LoggingTarget.Database, LogLevel.Error); + + // if we failed, let's delete the database and start fresh. + // todo: we probably want a better (non-destructive) migrations/recovery process at a later point than this. + contextFactory.ResetDatabase(); + Logger.Log("Database purged successfully.", LoggingTarget.Database, LogLevel.Important); + + using (var context = contextFactory.GetContext()) + context.Migrate(); + } } private WorkingBeatmap lastBeatmap; @@ -186,7 +208,7 @@ namespace osu.Game GlobalKeyBindingInputManager globalBinding; - base.Content.Add(new RatioAdjust + base.Content.Add(new DrawSizePreservingFillContainer { Children = new Drawable[] { @@ -208,10 +230,7 @@ namespace osu.Game // 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); - fpsDisplayVisible.ValueChanged += val => - { - FrameStatisticsMode = val ? FrameStatisticsMode.Minimal : FrameStatisticsMode.None; - }; + fpsDisplayVisible.ValueChanged += val => { FrameStatisticsMode = val ? FrameStatisticsMode.Minimal : FrameStatisticsMode.None; }; fpsDisplayVisible.TriggerChange(); } @@ -237,8 +256,6 @@ namespace osu.Game LocalConfig.Save(); } - connection?.Dispose(); - base.Dispose(isDisposing); } } diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index b0e4e49e31..46ee5a9cdb 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -9,6 +9,9 @@ using osu.Game.Graphics.Sprites; using osu.Game.Users; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Game.Graphics.Containers; +using osu.Framework.Graphics.Cursor; namespace osu.Game.Overlays.BeatmapSet { @@ -17,8 +20,11 @@ namespace osu.Game.Overlays.BeatmapSet private const float height = 50; private readonly UpdateableAvatar avatar; + private readonly ClickableArea clickableArea; private readonly FillFlowContainer fields; + private UserProfileOverlay profile; + private BeatmapSetInfo beatmapSet; public BeatmapSetInfo BeatmapSet { @@ -30,10 +36,12 @@ namespace osu.Game.Overlays.BeatmapSet var i = BeatmapSet.OnlineInfo; - avatar.User = i.Author; + avatar.User = BeatmapSet.Metadata.Author; + clickableArea.Action = () => profile?.ShowUser(avatar.User); + fields.Children = new Drawable[] { - new Field("made by", i.Author.Username, @"Exo2.0-RegularItalic"), + new Field("made by", BeatmapSet.Metadata.Author.Username, @"Exo2.0-RegularItalic"), new Field("submitted on", i.Submitted.ToString(@"MMM d, yyyy"), @"Exo2.0-Bold") { Margin = new MarginPadding { Top = 5 }, @@ -58,11 +66,15 @@ namespace osu.Game.Overlays.BeatmapSet Children = new Drawable[] { - avatar = new UpdateableAvatar + clickableArea = new ClickableArea { - Size = new Vector2(height), + AutoSizeAxes = Axes.Both, CornerRadius = 3, Masking = true, + Child = avatar = new UpdateableAvatar + { + Size = new Vector2(height), + }, EdgeEffect = new EdgeEffectParameters { Colour = Color4.Black.Opacity(0.25f), @@ -80,6 +92,13 @@ namespace osu.Game.Overlays.BeatmapSet }; } + [BackgroundDependencyLoader(true)] + private void load(UserProfileOverlay profile) + { + this.profile = profile; + clickableArea.Action = () => profile?.ShowUser(avatar.User); + } + private class Field : FillFlowContainer { public Field(string first, string second, string secondFont) @@ -103,5 +122,10 @@ namespace osu.Game.Overlays.BeatmapSet }; } } + + private class ClickableArea : OsuClickableContainer, IHasTooltip + { + public string TooltipText => @"View Profile"; + } } } diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 885f9cc219..3ecff85bee 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -39,9 +39,9 @@ namespace osu.Game.Overlays.BeatmapSet if (value == beatmap) return; beatmap = value; - length.Value = TimeSpan.FromMilliseconds(beatmap.OnlineInfo.Length).ToString(@"m\:ss"); - circleCount.Value = beatmap.OnlineInfo.CircleCount.ToString("N0"); - sliderCount.Value = beatmap.OnlineInfo.SliderCount.ToString("N0"); + length.Value = TimeSpan.FromSeconds(beatmap.OnlineInfo.Length).ToString(@"m\:ss"); + circleCount.Value = beatmap.OnlineInfo.CircleCount.ToString(); + sliderCount.Value = beatmap.OnlineInfo.SliderCount.ToString(); } } diff --git a/osu.Game/Overlays/BeatmapSet/Details.cs b/osu.Game/Overlays/BeatmapSet/Details.cs index 2fd0a55d9e..128ea7d2d3 100644 --- a/osu.Game/Overlays/BeatmapSet/Details.cs +++ b/osu.Game/Overlays/BeatmapSet/Details.cs @@ -88,6 +88,8 @@ namespace osu.Game.Overlays.BeatmapSet }; } + public void StopPreview() => preview.Playing.Value = false; + private class DetailBox : Container { private readonly Container content; diff --git a/osu.Game/Overlays/BeatmapSet/DownloadButton.cs b/osu.Game/Overlays/BeatmapSet/DownloadButton.cs index 18a0cfd968..47787d2ced 100644 --- a/osu.Game/Overlays/BeatmapSet/DownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/DownloadButton.cs @@ -14,10 +14,10 @@ namespace osu.Game.Overlays.BeatmapSet public DownloadButton(string title, string subtitle) { Width = 120; - RelativeSizeAxes = Axes.Y; - Child = new Container + Add(new Container { + Depth = -1, RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 10 }, Children = new Drawable[] @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapSet Margin = new MarginPadding { Right = 5 }, }, }, - }; + }); } } } diff --git a/osu.Game/Overlays/BeatmapSet/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/FavouriteButton.cs index 9fd4ac177c..1b22853656 100644 --- a/osu.Game/Overlays/BeatmapSet/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/FavouriteButton.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.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,13 +16,12 @@ namespace osu.Game.Overlays.BeatmapSet { public readonly Bindable Favourited = new Bindable(); - public FavouriteButton() + [BackgroundDependencyLoader] + private void load() { - RelativeSizeAxes = Axes.Y; - Container pink; SpriteIcon icon; - Children = new Drawable[] + AddRange(new Drawable[] { pink = new Container { @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.BeatmapSet Size = new Vector2(18), Shadow = false, }, - }; + }); Favourited.ValueChanged += value => { diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index a93ccbf704..4135aef268 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -26,8 +26,13 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Box tabsBg; private readonly Container coverContainer; private readonly OsuSpriteText title, artist; + private readonly Container noVideoButtons; + private readonly FillFlowContainer videoButtons; private readonly AuthorInfo author; - private readonly Details details; + private readonly Container downloadButtonsContainer; + public Details Details; + + private BeatmapManager beatmaps; private DelayedLoadWrapper cover; @@ -42,25 +47,26 @@ namespace osu.Game.Overlays.BeatmapSet if (value == beatmapSet) return; beatmapSet = value; - Picker.BeatmapSet = author.BeatmapSet = details.BeatmapSet = BeatmapSet; + Picker.BeatmapSet = author.BeatmapSet = Details.BeatmapSet = BeatmapSet; title.Text = BeatmapSet.Metadata.Title; artist.Text = BeatmapSet.Metadata.Artist; + downloadButtonsContainer.FadeIn(); + noVideoButtons.FadeTo(BeatmapSet.OnlineInfo.HasVideo ? 0 : 1, transition_duration); + videoButtons.FadeTo(BeatmapSet.OnlineInfo.HasVideo ? 1 : 0, transition_duration); + cover?.FadeOut(400, Easing.Out); - coverContainer.Add(cover = new DelayedLoadWrapper(new BeatmapSetCover(BeatmapSet) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fill, - OnLoadComplete = d => + coverContainer.Add(cover = new DelayedLoadWrapper( + new BeatmapSetCover(BeatmapSet) { - d.FadeInFromZero(400, Easing.Out); - }, - }) + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), + }, 300) { RelativeSizeAxes = Axes.Both, - TimeBeforeLoad = 300 }); } } @@ -77,9 +83,6 @@ namespace osu.Game.Overlays.BeatmapSet Radius = 3, Offset = new Vector2(0f, 1f), }; - - Container noVideoButtons; - FillFlowContainer videoButtons; Children = new Drawable[] { new Container @@ -162,7 +165,7 @@ namespace osu.Game.Overlays.BeatmapSet Children = new Drawable[] { new FavouriteButton(), - new Container + downloadButtonsContainer = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = buttons_height + buttons_spacing }, @@ -172,7 +175,10 @@ namespace osu.Game.Overlays.BeatmapSet { RelativeSizeAxes = Axes.Both, Alpha = 0f, - Child = new DownloadButton("Download", @""), + Child = new DownloadButton("Download", @"") + { + Action = () => download(false), + }, }, videoButtons = new FillFlowContainer { @@ -181,8 +187,14 @@ namespace osu.Game.Overlays.BeatmapSet Alpha = 0f, Children = new[] { - new DownloadButton("Download", "with Video"), - new DownloadButton("Download", "without Video"), + new DownloadButton("Download", "with Video") + { + Action = () => download(false), + }, + new DownloadButton("Download", "without Video") + { + Action = () => download(true), + }, }, }, }, @@ -192,7 +204,7 @@ namespace osu.Game.Overlays.BeatmapSet }, }, }, - details = new Details + Details = new Details { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, @@ -202,27 +214,43 @@ namespace osu.Game.Overlays.BeatmapSet }, }; - Picker.Beatmap.ValueChanged += b => - { - details.Beatmap = b; - - if (b.OnlineInfo.HasVideo) - { - noVideoButtons.FadeOut(transition_duration); - videoButtons.FadeIn(transition_duration); - } - else - { - noVideoButtons.FadeIn(transition_duration); - videoButtons.FadeOut(transition_duration); - } - }; + Picker.Beatmap.ValueChanged += b => Details.Beatmap = b; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, BeatmapManager beatmaps) { tabsBg.Colour = colours.Gray3; + this.beatmaps = beatmaps; + + beatmaps.BeatmapSetAdded += handleBeatmapAdd; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + if (beatmaps != null) beatmaps.BeatmapSetAdded -= handleBeatmapAdd; + } + + private void handleBeatmapAdd(BeatmapSetInfo beatmap) + { + if (beatmap.OnlineBeatmapSetID == BeatmapSet?.OnlineBeatmapSetID) + downloadButtonsContainer.FadeOut(transition_duration); + } + + private void download(bool noVideo) + { + if (beatmaps.GetExistingDownload(BeatmapSet) != null) + { + downloadButtonsContainer.MoveToX(-5, 50, Easing.OutSine).Then() + .MoveToX(5, 100, Easing.InOutSine).Then() + .MoveToX(-5, 100, Easing.InOutSine).Then() + .MoveToX(0, 50, Easing.InSine).Then(); + + return; + } + + beatmaps.Download(BeatmapSet, noVideo); } } } diff --git a/osu.Game/Overlays/BeatmapSet/HeaderButton.cs b/osu.Game/Overlays/BeatmapSet/HeaderButton.cs index 3075020fe6..ac5683de00 100644 --- a/osu.Game/Overlays/BeatmapSet/HeaderButton.cs +++ b/osu.Game/Overlays/BeatmapSet/HeaderButton.cs @@ -2,44 +2,27 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Framework.Allocation; namespace osu.Game.Overlays.BeatmapSet { - public class HeaderButton : OsuClickableContainer + public class HeaderButton : TriangleButton { - private readonly Container content; - - protected override Container Content => content; - public HeaderButton() { - CornerRadius = 3; - Masking = true; + Height = 0; + RelativeSizeAxes = Axes.Y; + } - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"094c5f"), - }, - new Triangles - { - RelativeSizeAxes = Axes.Both, - ColourLight = OsuColour.FromHex(@"0f7c9b"), - ColourDark = OsuColour.FromHex(@"094c5f"), - TriangleScale = 1.5f, - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, - }; + [BackgroundDependencyLoader] + private void load() + { + BackgroundColour = OsuColour.FromHex(@"094c5f"); + Triangles.ColourLight = OsuColour.FromHex(@"0f7c9b"); + Triangles.ColourDark = OsuColour.FromHex(@"094c5f"); + Triangles.TriangleScale = 1.5f; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index 4a59591a72..bd108a193b 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -74,20 +74,18 @@ namespace osu.Game.Overlays.BeatmapSet { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = metadata_width + BeatmapSetOverlay.RIGHT_WIDTH + spacing * 2 }, - Child = new ScrollContainer + Child = new Container { RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, Child = description = new MetadataSection("Description"), }, }, - new ScrollContainer + new Container { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = metadata_width, - ScrollbarVisible = false, Padding = new MarginPadding { Horizontal = 10 }, Margin = new MarginPadding { Right = BeatmapSetOverlay.RIGHT_WIDTH + spacing }, Child = new FillFlowContainer diff --git a/osu.Game/Overlays/BeatmapSet/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/PreviewButton.cs index bdb06106f0..52edd1714f 100644 --- a/osu.Game/Overlays/BeatmapSet/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/PreviewButton.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.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -12,9 +11,10 @@ using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; using OpenTK; using OpenTK.Graphics; +using osu.Game.Overlays.Direct; +using osu.Framework.Configuration; namespace osu.Game.Overlays.BeatmapSet { @@ -22,73 +22,16 @@ namespace osu.Game.Overlays.BeatmapSet { private const float transition_duration = 500; - private readonly Container audioWrapper; private readonly Box bg, progress; - private readonly SpriteIcon icon; - private readonly LoadingAnimation loadingAnimation; + private readonly PlayButton playButton; - private Track preview; + private Track preview => playButton.Preview; + public Bindable Playing => playButton.Playing; - private bool loading - { - set - { - if (value) - { - loadingAnimation.Show(); - icon.FadeOut(transition_duration * 5, Easing.OutQuint); - } - else - { - loadingAnimation.Hide(); - icon.FadeIn(transition_duration, Easing.OutQuint); - } - } - } - - private BeatmapSetInfo beatmapSet; public BeatmapSetInfo BeatmapSet { - get { return beatmapSet; } - set - { - if (value == beatmapSet) return; - beatmapSet = value; - - Playing = false; - preview = null; - } - } - - private bool playing; - public bool Playing - { - get { return playing; } - set - { - if (value == playing) return; - playing = value; - - if (preview == null) - { - loading = true; - audioWrapper.Child = new AsyncLoadWrapper(new AudioLoadWrapper(BeatmapSet) - { - OnLoadComplete = d => - { - loading = false; - - preview = (d as AudioLoadWrapper)?.Preview; - Playing = Playing; - updatePlayingState(); - }, - }); - - return; - } - - updatePlayingState(); - } + get { return playButton.BeatmapSet; } + set { playButton.BeatmapSet = value; } } public PreviewButton() @@ -97,7 +40,6 @@ namespace osu.Game.Overlays.BeatmapSet Children = new Drawable[] { - audioWrapper = new Container(), bg = new Box { RelativeSizeAxes = Axes.Both, @@ -116,22 +58,16 @@ namespace osu.Game.Overlays.BeatmapSet Alpha = 0f, }, }, - icon = new SpriteIcon + playButton = new PlayButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Icon = FontAwesome.fa_play, Size = new Vector2(18), - Shadow = false, - }, - loadingAnimation = new LoadingAnimation - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, }, }; - Action = () => Playing = !Playing; + Action = () => Playing.Value = !Playing.Value; + Playing.ValueChanged += newValue => progress.FadeTo(newValue ? 1 : 0, 100); } [BackgroundDependencyLoader] @@ -144,20 +80,15 @@ namespace osu.Game.Overlays.BeatmapSet { base.Update(); - if (Playing && preview != null) + if (Playing.Value && preview != null) { progress.Width = (float)(preview.CurrentTime / preview.Length); - if (preview.HasCompleted) - { - Playing = false; - preview = null; - } } } protected override void Dispose(bool isDisposing) { - Playing = false; + Playing.Value = false; base.Dispose(isDisposing); } @@ -172,47 +103,5 @@ namespace osu.Game.Overlays.BeatmapSet bg.FadeColour(Color4.Black.Opacity(0.25f), 100); base.OnHoverLost(state); } - - private void updatePlayingState() - { - if (preview == null) return; - - if (Playing) - { - icon.Icon = FontAwesome.fa_stop; - progress.FadeIn(100); - - preview.Seek(0); - preview.Start(); - } - else - { - icon.Icon = FontAwesome.fa_play; - progress.FadeOut(100); - preview.Stop(); - } - } - - private class AudioLoadWrapper : Drawable - { - private readonly string preview; - - public Track Preview; - - public AudioLoadWrapper(BeatmapSetInfo set) - { - preview = set.OnlineInfo.Preview; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - if (!string.IsNullOrEmpty(preview)) - { - Preview = audio.Track.Get(preview); - Preview.Volume.Value = 0.5; - } - } - } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ClickableUsername.cs b/osu.Game/Overlays/BeatmapSet/Scores/ClickableUsername.cs new file mode 100644 index 0000000000..bbb4ef767c --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/Scores/ClickableUsername.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Users; +using osu.Framework.Input; + +namespace osu.Game.Overlays.BeatmapSet.Scores +{ + public class ClickableUsername : OsuHoverContainer + { + private readonly OsuSpriteText text; + private UserProfileOverlay profile; + + private User user; + public User User + { + get { return user; } + set + { + if (user == value) return; + user = value; + + text.Text = user.Username; + } + } + + public float TextSize + { + set + { + if (text.TextSize == value) return; + text.TextSize = value; + } + get { return text.TextSize; } + } + + public ClickableUsername() + { + AutoSizeAxes = Axes.Both; + Child = text = new OsuSpriteText + { + Font = @"Exo2.0-BoldItalic", + }; + } + + [BackgroundDependencyLoader(true)] + private void load(UserProfileOverlay profile) + { + this.profile = profile; + } + + protected override bool OnClick(InputState state) + { + profile?.ShowUser(user); + return true; + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/Scores/DrawableScore.cs b/osu.Game/Overlays/BeatmapSet/Scores/DrawableScore.cs new file mode 100644 index 0000000000..5a3aba7b43 --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/Scores/DrawableScore.cs @@ -0,0 +1,141 @@ +// 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.Allocation; +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.Sprites; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Profile.Sections.Ranks; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Users; + +namespace osu.Game.Overlays.BeatmapSet.Scores +{ + public class DrawableScore : Container + { + private const int fade_duration = 100; + private const float side_margin = 20; + + private readonly Box background; + + public DrawableScore(int index, OnlineScore score) + { + ScoreModsContainer modsContainer; + + RelativeSizeAxes = Axes.X; + Height = 30; + CornerRadius = 3; + Masking = true; + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = $"#{index + 1}", + Font = @"Exo2.0-RegularItalic", + Margin = new MarginPadding { Left = side_margin } + }, + new DrawableFlag(score.User.Country?.FlagName) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 20), + Margin = new MarginPadding { Left = 60 } + }, + new ClickableUsername + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + User = score.User, + Margin = new MarginPadding { Left = 100 } + }, + modsContainer = new ScoreModsContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Width = 0.06f, + RelativePositionAxes = Axes.X, + X = 0.42f + }, + new DrawableRank(score.Rank) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 20), + FillMode = FillMode.Fit, + RelativePositionAxes = Axes.X, + X = 0.55f + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Text = $@"{score.TotalScore:N0}", + Font = @"Venera", + RelativePositionAxes = Axes.X, + X = 0.75f, + FixedWidth = true, + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Text = $@"{score.Accuracy:P2}", + Font = @"Exo2.0-RegularItalic", + RelativePositionAxes = Axes.X, + X = 0.85f + }, + new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Text = $"{score.Statistics["300"]}/{score.Statistics["100"]}/{score.Statistics["50"]}", + Font = @"Exo2.0-RegularItalic", + Margin = new MarginPadding { Right = side_margin } + }, + }; + + foreach (Mod mod in score.Mods) + modsContainer.Add(new ModIcon(mod) + { + AutoSizeAxes = Axes.Both, + Scale = new Vector2(0.35f), + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Gray4; + } + + protected override bool OnHover(InputState state) + { + background.FadeIn(fade_duration, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + background.FadeOut(fade_duration, Easing.OutQuint); + base.OnHoverLost(state); + } + + protected override bool OnClick(InputState state) => true; + } +} diff --git a/osu.Game/Overlays/BeatmapSet/Scores/DrawableTopScore.cs b/osu.Game/Overlays/BeatmapSet/Scores/DrawableTopScore.cs new file mode 100644 index 0000000000..833ed94c0f --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/Scores/DrawableTopScore.cs @@ -0,0 +1,243 @@ +// 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; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Profile.Sections.Ranks; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Users; + +namespace osu.Game.Overlays.BeatmapSet.Scores +{ + public class DrawableTopScore : Container + { + private const float fade_duration = 100; + private const float height = 200; + private const float avatar_size = 80; + private const float margin = 10; + + private readonly Box background; + private readonly Box bottomBackground; + private readonly Box middleLine; + private readonly UpdateableAvatar avatar; + private readonly DrawableFlag flag; + private readonly ClickableUsername username; + private readonly OsuSpriteText rankText; + private readonly OsuSpriteText date; + private readonly DrawableRank rank; + private readonly InfoColumn totalScore; + private readonly InfoColumn accuracy; + private readonly InfoColumn statistics; + private readonly ScoreModsContainer modsContainer; + + private OnlineScore score; + public OnlineScore Score + { + get { return score; } + set + { + if (score == value) return; + score = value; + + avatar.User = username.User = score.User; + flag.FlagName = score.User.Country?.FlagName; + date.Text = $@"achieved {score.Date:MMM d, yyyy}"; + rank.UpdateRank(score.Rank); + + totalScore.Value = $@"{score.TotalScore:N0}"; + accuracy.Value = $@"{score.Accuracy:P2}"; + statistics.Value = $"{score.Statistics["300"]}/{score.Statistics["100"]}/{score.Statistics["50"]}"; + + modsContainer.Clear(); + foreach (Mod mod in score.Mods) + modsContainer.Add(new ModIcon(mod) + { + AutoSizeAxes = Axes.Both, + Scale = new Vector2(0.45f), + }); + } + } + + public DrawableTopScore() + { + RelativeSizeAxes = Axes.X; + Height = height; + CornerRadius = 5; + BorderThickness = 4; + Masking = true; + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, //used for correct border representation + }, + avatar = new UpdateableAvatar + { + Size = new Vector2(avatar_size), + Masking = true, + CornerRadius = 5, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Offset = new Vector2(0, 2), + Radius = 1, + }, + Margin = new MarginPadding { Top = margin, Left = margin } + }, + flag = new DrawableFlag + { + Size = new Vector2(30, 20), + Position = new Vector2(margin * 2 + avatar_size, height / 4), + }, + username = new ClickableUsername + { + Origin = Anchor.BottomLeft, + TextSize = 30, + Position = new Vector2(margin * 2 + avatar_size, height / 4), + Margin = new MarginPadding { Bottom = 4 } + }, + rankText = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.BottomRight, + Text = "#1", + TextSize = 40, + Font = @"Exo2.0-BoldItalic", + Y = height / 4, + Margin = new MarginPadding { Right = margin } + }, + date = new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Y = height / 4, + Margin = new MarginPadding { Right = margin } + }, + new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + Children = new Drawable[] + { + bottomBackground = new Box { RelativeSizeAxes = Axes.Both }, + middleLine = new Box + { + RelativeSizeAxes = Axes.X, + Height = 1, + }, + rank = new DrawableRank(ScoreRank.F) + { + Origin = Anchor.BottomLeft, + Size = new Vector2(avatar_size, 40), + FillMode = FillMode.Fit, + Y = height / 4, + Margin = new MarginPadding { Left = margin } + }, + new FillFlowContainer + { + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Position = new Vector2(height / 2, height / 4), + Direction = FillDirection.Horizontal, + Spacing = new Vector2(15, 0), + Children = new[] + { + totalScore = new InfoColumn("Score"), + accuracy = new InfoColumn("Accuracy"), + statistics = new InfoColumn("300/100/50"), + }, + }, + modsContainer = new ScoreModsContainer + { + AutoSizeAxes = Axes.Y, + Width = 80, + Position = new Vector2(height / 2, height / 4), + } + } + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = bottomBackground.Colour = colours.Gray4; + middleLine.Colour = colours.Gray2; + date.Colour = colours.Gray9; + BorderColour = rankText.Colour = colours.Yellow; + } + + protected override bool OnHover(InputState state) + { + background.FadeIn(fade_duration, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + background.FadeOut(fade_duration, Easing.OutQuint); + base.OnHoverLost(state); + } + + private class InfoColumn : FillFlowContainer + { + private readonly OsuSpriteText headerText; + private readonly OsuSpriteText valueText; + + public string Value + { + set + { + if (valueText.Text == value) + return; + valueText.Text = value; + } + get { return valueText.Text; } + } + + public InfoColumn(string header) + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Vertical; + Spacing = new Vector2(0, 3); + Children = new Drawable[] + { + headerText = new OsuSpriteText + { + TextSize = 14, + Text = header, + Font = @"Exo2.0-Bold", + }, + valueText = new OsuSpriteText + { + TextSize = 25, + Font = @"Exo2.0-RegularItalic", + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + headerText.Colour = colours.Gray9; + } + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs new file mode 100644 index 0000000000..4b496ed118 --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -0,0 +1,115 @@ +// 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.Shapes; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API.Requests; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Overlays.BeatmapSet.Scores +{ + public class ScoresContainer : Container + { + private const int spacing = 15; + private const int fade_duration = 200; + + private readonly FillFlowContainer flow; + private readonly DrawableTopScore topScore; + private readonly LoadingAnimation loadingAnimation; + private readonly Box foreground; + + private bool isLoading; + public bool IsLoading + { + get { return isLoading; } + set + { + if (isLoading == value) return; + isLoading = value; + + foreground.FadeTo(isLoading ? 1 : 0, fade_duration); + loadingAnimation.FadeTo(isLoading ? 1 : 0, fade_duration); + } + } + + private IEnumerable scores; + public IEnumerable Scores + { + get { return scores; } + set + { + scores = value; + var scoresAmount = scores.Count(); + if (scoresAmount == 0) + { + CleanAllScores(); + return; + } + + topScore.Score = scores.FirstOrDefault(); + topScore.Show(); + + flow.Clear(); + + if (scoresAmount < 2) + return; + + for (int i = 1; i < scoresAmount; i++) + flow.Add(new DrawableScore(i, scores.ElementAt(i))); + } + } + + public ScoresContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Width = 0.95f, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, spacing), + Margin = new MarginPadding { Vertical = spacing }, + Children = new Drawable[] + { + topScore = new DrawableTopScore(), + flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 1), + }, + } + }, + foreground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.7f), + Alpha = 0, + }, + loadingAnimation = new LoadingAnimation + { + Alpha = 0, + }, + }; + } + + public void CleanAllScores() + { + topScore.Hide(); + flow.Clear(); + } + } +} diff --git a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs index 26335aac9b..6df31b9f85 100644 --- a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs +++ b/osu.Game/Overlays/BeatmapSet/SuccessRate.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 System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -30,8 +29,11 @@ namespace osu.Game.Overlays.BeatmapSet if (value == beatmap) return; beatmap = value; - var rate = (float)beatmap.OnlineInfo.PassCount / beatmap.OnlineInfo.PlayCount; - successPercent.Text = $"{Math.Round(rate * 100)}%"; + int passCount = beatmap.OnlineInfo.PassCount; + int playCount = beatmap.OnlineInfo.PlayCount; + + var rate = playCount != 0 ? (float)passCount / playCount : 0; + successPercent.Text = rate.ToString("P0"); successRate.Length = rate; percentContainer.ResizeWidthTo(successRate.Length, 250, Easing.InOutCubic); diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 8e28ad33c5..0a88f586b5 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.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.Allocation; using OpenTK; using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; @@ -11,7 +12,11 @@ using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Overlays.BeatmapSet; +using osu.Game.Rulesets; +using osu.Game.Overlays.BeatmapSet.Scores; namespace osu.Game.Overlays { @@ -22,6 +27,16 @@ namespace osu.Game.Overlays private readonly Header header; private readonly Info info; + private readonly ScoresContainer scores; + + private APIAccess api; + private RulesetStore rulesets; + private GetScoresRequest getScoresRequest; + + private readonly ScrollContainer scroll; + + // receive input outside our bounds so we can trigger a close event on ourselves. + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; public BeatmapSetOverlay() { @@ -51,7 +66,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.2f) }, - new ScrollContainer + scroll = new ScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, @@ -64,12 +79,45 @@ namespace osu.Game.Overlays { header = new Header(), info = new Info(), + scores = new ScoresContainer(), }, }, }, }; - header.Picker.Beatmap.ValueChanged += b => info.Beatmap = b; + header.Picker.Beatmap.ValueChanged += b => + { + info.Beatmap = b; + updateScores(b); + }; + } + + private void updateScores(BeatmapInfo beatmap) + { + getScoresRequest?.Cancel(); + + if (!beatmap.OnlineBeatmapID.HasValue) + { + scores.CleanAllScores(); + return; + } + + scores.IsLoading = true; + + getScoresRequest = new GetScoresRequest(beatmap, beatmap.Ruleset); + getScoresRequest.Success += r => + { + scores.Scores = r.Scores; + scores.IsLoading = false; + }; + api.Queue(getScoresRequest); + } + + [BackgroundDependencyLoader] + private void load(APIAccess api, RulesetStore rulesets) + { + this.api = api; + this.rulesets = rulesets; } protected override void PopIn() @@ -81,6 +129,7 @@ namespace osu.Game.Overlays protected override void PopOut() { base.PopOut(); + header.Details.StopPreview(); FadeEdgeEffectTo(0, DISAPPEAR_DURATION, Easing.Out); } @@ -90,10 +139,19 @@ namespace osu.Game.Overlays return true; } + public void ShowBeatmapSet(int beatmapSetId) + { + // todo: display the overlay while we are loading here. we need to support setting BeatmapSet to null for this to work. + var req = new GetBeatmapSetRequest(beatmapSetId); + req.Success += res => ShowBeatmapSet(res.ToBeatmapSet(rulesets)); + api.Queue(req); + } + public void ShowBeatmapSet(BeatmapSetInfo set) { header.BeatmapSet = info.BeatmapSet = set; Show(); + scroll.ScrollTo(0); } } } diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 4db6bdf5e4..32f933ff42 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -222,7 +222,7 @@ namespace osu.Game.Overlays.Chat } - private class MessageSender : ClickableContainer, IHasContextMenu + private class MessageSender : OsuClickableContainer, IHasContextMenu { private readonly User sender; diff --git a/osu.Game/Overlays/Chat/ChatTabControl.cs b/osu.Game/Overlays/Chat/ChatTabControl.cs index 9f1028c168..f58ee8f819 100644 --- a/osu.Game/Overlays/Chat/ChatTabControl.cs +++ b/osu.Game/Overlays/Chat/ChatTabControl.cs @@ -17,6 +17,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Configuration; using System; +using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Chat { @@ -259,7 +260,7 @@ namespace osu.Game.Overlays.Chat }; } - public class CloseButton : ClickableContainer + public class CloseButton : OsuClickableContainer { private readonly SpriteIcon icon; diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 24fc322199..9f40a08ad2 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -58,7 +58,7 @@ namespace osu.Game.Overlays private readonly Box chatBackground; private readonly Box tabBackground; - public Bindable ChatHeight { get; internal set; } + public Bindable ChatHeight { get; set; } private readonly Container channelSelectionContainer; private readonly ChannelSelectionOverlay channelSelection; diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/Direct/DirectGridPanel.cs index 1675a2f663..a40f20850e 100644 --- a/osu.Game/Overlays/Direct/DirectGridPanel.cs +++ b/osu.Game/Overlays/Direct/DirectGridPanel.cs @@ -21,9 +21,15 @@ namespace osu.Game.Overlays.Direct private const float vertical_padding = 5; private FillFlowContainer bottomPanel; + private PlayButton playButton; + private Box progressBar; + + protected override PlayButton PlayButton => playButton; + protected override Box PreviewBar => progressBar; public DirectGridPanel(BeatmapSetInfo beatmap) : base(beatmap) { + Width = 400; Height = 140 + vertical_padding; //full height of all the elements plus vertical padding (autosize uses the image) } @@ -87,6 +93,15 @@ namespace osu.Game.Overlays.Direct { RelativeSizeAxes = Axes.Both, }, + progressBar = new Box + { + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + BypassAutoSizeAxes = Axes.Both, + Size = new Vector2(0, 3), + Alpha = 0, + Colour = colours.Yellow, + }, new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -116,7 +131,7 @@ namespace osu.Game.Overlays.Direct }, new OsuSpriteText { - Text = SetInfo.Metadata.Author, + Text = SetInfo.Metadata.Author.Username, TextSize = 14, Font = @"Exo2.0-SemiBoldItalic", Shadow = false, @@ -178,6 +193,12 @@ namespace osu.Game.Overlays.Direct new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0), }, }, + playButton = new PlayButton(SetInfo) + { + Margin = new MarginPadding { Top = 5, Left = 10 }, + Size = new Vector2(30), + Alpha = 0, + }, }); } } diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index 6702b7394c..3a1004fd44 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -28,8 +28,14 @@ namespace osu.Game.Overlays.Direct Height = height; } + private PlayButton playButton; + private Box progressBar; + + protected override PlayButton PlayButton => playButton; + protected override Box PreviewBar => progressBar; + [BackgroundDependencyLoader] - private void load(LocalisationEngine localisation) + private void load(LocalisationEngine localisation, OsuColour colours) { Content.CornerRadius = 5; @@ -48,29 +54,50 @@ namespace osu.Game.Overlays.Direct { new FillFlowContainer { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, + Direction = FillDirection.Horizontal, + LayoutEasing = Easing.OutQuint, + LayoutDuration = 120, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OsuSpriteText + playButton = new PlayButton(SetInfo) { - Current = localisation.GetUnicodePreference(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), - TextSize = 18, - Font = @"Exo2.0-BoldItalic", - }, - new OsuSpriteText - { - Current = localisation.GetUnicodePreference(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), - Font = @"Exo2.0-BoldItalic", + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Size = new Vector2(height / 2), + FillMode = FillMode.Fit, + Alpha = 0, }, new FillFlowContainer { - AutoSizeAxes = Axes.X, - Height = 20, - Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding }, - Children = GetDifficultyIcons(), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Current = localisation.GetUnicodePreference(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), + TextSize = 18, + Font = @"Exo2.0-BoldItalic", + }, + new OsuSpriteText + { + Current = localisation.GetUnicodePreference(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), + Font = @"Exo2.0-BoldItalic", + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.X, + Height = 20, + Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding }, + Children = GetDifficultyIcons(), + }, + }, }, - }, + } }, new FillFlowContainer { @@ -101,7 +128,7 @@ namespace osu.Game.Overlays.Direct }, new OsuSpriteText { - Text = SetInfo.Metadata.Author, + Text = SetInfo.Metadata.Author.Username, TextSize = 14, Font = @"Exo2.0-SemiBoldItalic", }, @@ -126,6 +153,16 @@ namespace osu.Game.Overlays.Direct }, }, }, + progressBar = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + BypassAutoSizeAxes = Axes.Y, + Size = new Vector2(0, 3), + Alpha = 0, + Colour = colours.Yellow, + }, }); } } diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index 4f7f1bb39e..5980cde5fd 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -16,10 +16,10 @@ using osu.Game.Graphics.Sprites; using OpenTK.Graphics; using osu.Framework.Input; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.API; using osu.Framework.Logging; -using osu.Game.Overlays.Notifications; using osu.Game.Online.API.Requests; +using osu.Framework.Configuration; +using osu.Framework.Audio.Track; namespace osu.Game.Overlays.Direct { @@ -33,12 +33,15 @@ namespace osu.Game.Overlays.Direct private Container content; - private APIAccess api; private ProgressBar progressBar; private BeatmapManager beatmaps; - private NotificationOverlay notifications; private BeatmapSetOverlay beatmapSetOverlay; + public Track Preview => PlayButton.Preview; + public Bindable PreviewPlaying => PlayButton.Playing; + protected abstract PlayButton PlayButton { get; } + protected abstract Box PreviewBar { get; } + protected override Container Content => content; protected DirectPanel(BeatmapSetInfo setInfo) @@ -64,11 +67,9 @@ namespace osu.Game.Overlays.Direct [BackgroundDependencyLoader(permitNulls: true)] - private void load(APIAccess api, BeatmapManager beatmaps, OsuColour colours, NotificationOverlay notifications, BeatmapSetOverlay beatmapSetOverlay) + private void load(BeatmapManager beatmaps, OsuColour colours, BeatmapSetOverlay beatmapSetOverlay) { - this.api = api; this.beatmaps = beatmaps; - this.notifications = notifications; this.beatmapSetOverlay = beatmapSetOverlay; AddInternal(content = new Container @@ -102,12 +103,31 @@ namespace osu.Game.Overlays.Direct if (downloadRequest != null) attachDownload(downloadRequest); + + beatmaps.BeatmapDownloadBegan += attachDownload; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + beatmaps.BeatmapDownloadBegan -= attachDownload; + } + + protected override void Update() + { + base.Update(); + + if (PreviewPlaying && Preview != null && Preview.IsLoaded) + { + PreviewBar.Width = (float)(Preview.CurrentTime / Preview.Length); + } } protected override bool OnHover(InputState state) { content.TweenEdgeEffectTo(edgeEffectHovered, hover_transition_time, Easing.OutQuint); content.MoveToY(-4, hover_transition_time, Easing.OutQuint); + PlayButton.FadeIn(120, Easing.InOutQuint); return base.OnHover(state); } @@ -116,6 +136,8 @@ namespace osu.Game.Overlays.Direct { content.TweenEdgeEffectTo(edgeEffectNormal, hover_transition_time, Easing.OutQuint); content.MoveToY(0, hover_transition_time, Easing.OutQuint); + if (!PreviewPlaying) + PlayButton.FadeOut(120, Easing.InOutQuint); base.OnHoverLost(state); } @@ -123,6 +145,7 @@ namespace osu.Game.Overlays.Direct protected override bool OnClick(InputState state) { ShowInformation(); + PreviewPlaying.Value = false; return true; } @@ -130,16 +153,6 @@ namespace osu.Game.Overlays.Direct protected void StartDownload() { - if (!api.LocalUser.Value.IsSupporter) - { - notifications.Post(new SimpleNotification - { - Icon = FontAwesome.fa_superpowers, - Text = "You gotta be a supporter to download for now 'yo" - }); - return; - } - if (beatmaps.GetExistingDownload(SetInfo) != null) { // we already have an active download running. @@ -151,13 +164,14 @@ namespace osu.Game.Overlays.Direct return; } - var request = beatmaps.Download(SetInfo); - - attachDownload(request); + beatmaps.Download(SetInfo); } private void attachDownload(DownloadBeatmapSetRequest request) { + if (request.BeatmapSet.OnlineBeatmapSetID != SetInfo.OnlineBeatmapSetID) + return; + progressBar.FadeIn(400, Easing.OutQuint); progressBar.ResizeHeightTo(4, 400, Easing.OutQuint); @@ -183,6 +197,9 @@ namespace osu.Game.Overlays.Direct { base.LoadComplete(); this.FadeInFromZero(200, Easing.Out); + + PreviewPlaying.ValueChanged += newValue => PlayButton.FadeTo(newValue || IsHovered ? 1 : 0, 120, Easing.InOutQuint); + PreviewPlaying.ValueChanged += newValue => PreviewBar.FadeTo(newValue ? 1 : 0, 120, Easing.InOutQuint); } protected List GetDifficultyIcons() @@ -195,21 +212,21 @@ namespace osu.Game.Overlays.Direct return icons; } - protected Drawable CreateBackground() => new DelayedLoadWrapper(new BeatmapSetCover(SetInfo) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fill, - OnLoadComplete = d => + protected Drawable CreateBackground() => new DelayedLoadWrapper( + new BeatmapSetCover(SetInfo) { - d.FadeInFromZero(400, Easing.Out); - BlackBackground.Delay(400).FadeOut(); - }, - }) + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + OnLoadComplete = d => + { + d.FadeInFromZero(400, Easing.Out); + BlackBackground.Delay(400).FadeOut(); + }, + }, 300) { RelativeSizeAxes = Axes.Both, - TimeBeforeLoad = 300 }; public class Statistic : FillFlowContainer diff --git a/osu.Game/Overlays/Direct/FilterControl.cs b/osu.Game/Overlays/Direct/FilterControl.cs index 28d26d0641..46ba000a28 100644 --- a/osu.Game/Overlays/Direct/FilterControl.cs +++ b/osu.Game/Overlays/Direct/FilterControl.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Direct DisplayStyleControl.Dropdown.AccentColour = colours.BlueDark; Ruleset.BindTo(game?.Ruleset ?? new Bindable { Value = rulesets.GetRuleset(0) }); - foreach (var r in rulesets.AllRulesets) + foreach (var r in rulesets.AvailableRulesets) { modeButtons.Add(new RulesetToggleButton(Ruleset, r)); } @@ -105,6 +105,7 @@ namespace osu.Game.Overlays.Direct public enum DirectSortCriteria { + Relevance, Title, Artist, Creator, diff --git a/osu.Game/Overlays/Direct/Header.cs b/osu.Game/Overlays/Direct/Header.cs index 2c50fb453f..77743a3a4b 100644 --- a/osu.Game/Overlays/Direct/Header.cs +++ b/osu.Game/Overlays/Direct/Header.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Direct public Header() { - Tabs.Current.Value = DirectTab.Search; + Tabs.Current.Value = DirectTab.NewestMaps; Tabs.Current.TriggerChange(); } } diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/Direct/PlayButton.cs new file mode 100644 index 0000000000..75a3358d51 --- /dev/null +++ b/osu.Game/Overlays/Direct/PlayButton.cs @@ -0,0 +1,184 @@ +// 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.Track; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Direct +{ + public class PlayButton : Container + { + public readonly Bindable Playing = new Bindable(); + public Track Preview { get; private set; } + + private BeatmapSetInfo beatmapSet; + public BeatmapSetInfo BeatmapSet + { + get { return beatmapSet; } + set + { + if (value == beatmapSet) return; + beatmapSet = value; + + Playing.Value = false; + trackLoader = null; + Preview = null; + } + } + + private Color4 hoverColour; + private readonly SpriteIcon icon; + private readonly LoadingAnimation loadingAnimation; + + private const float transition_duration = 500; + + private bool loading + { + set + { + if (value) + { + loadingAnimation.Show(); + icon.FadeOut(transition_duration * 5, Easing.OutQuint); + } + else + { + loadingAnimation.Hide(); + icon.FadeIn(transition_duration, Easing.OutQuint); + } + } + } + + public PlayButton(BeatmapSetInfo setInfo = null) + { + BeatmapSet = setInfo; + AddRange(new Drawable[] + { + icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.fa_play, + }, + loadingAnimation = new LoadingAnimation(), + }); + + Playing.ValueChanged += playing => + { + icon.Icon = playing ? FontAwesome.fa_pause : FontAwesome.fa_play; + icon.FadeColour(playing || IsHovered ? hoverColour : Color4.White, 120, Easing.InOutQuint); + updatePreviewTrack(playing); + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + hoverColour = colour.Yellow; + } + + protected override bool OnClick(InputState state) + { + Playing.Value = !Playing.Value; + return true; + } + + protected override bool OnHover(InputState state) + { + icon.FadeColour(hoverColour, 120, Easing.InOutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + if (!Playing.Value) + icon.FadeColour(Color4.White, 120, Easing.InOutQuint); + base.OnHoverLost(state); + } + + protected override void Update() + { + base.Update(); + + if (Preview?.HasCompleted ?? false) + { + Playing.Value = false; + Preview = null; + } + } + + private void updatePreviewTrack(bool playing) + { + if (playing) + { + if (Preview == null) + { + beginAudioLoad(); + return; + } + + Preview.Seek(0); + Preview.Start(); + } + else + { + Preview?.Stop(); + loading = false; + } + } + + private TrackLoader trackLoader; + + private void beginAudioLoad() + { + if (trackLoader != null) return; + + loading = true; + + LoadComponentAsync(trackLoader = new TrackLoader($"https://b.ppy.sh/preview/{BeatmapSet.OnlineBeatmapSetID}.mp3"), + d => + { + // We may have been replaced by another loader + if (trackLoader != d) return; + + Preview = d?.Preview; + Playing.TriggerChange(); + loading = false; + Add(trackLoader); + }); + } + + private class TrackLoader : Drawable + { + private readonly string preview; + + public Track Preview; + + public TrackLoader(string preview) + { + this.preview = preview; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + if (!string.IsNullOrEmpty(preview)) + { + Preview = audio.Track.Get(preview); + Preview.Volume.Value = 0.5; + } + } + } + } +} diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 9c07e1087f..7994483043 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using OpenTK; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -32,6 +33,7 @@ namespace osu.Game.Overlays private readonly FillFlowContainer resultCountsContainer; private readonly OsuSpriteText resultCountsText; private FillFlowContainer panels; + private DirectPanel playing; protected override Color4 BackgroundColour => OsuColour.FromHex(@"485e74"); protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"465b71"); @@ -41,6 +43,7 @@ namespace osu.Game.Overlays protected override SearchableListFilterControl CreateFilterControl() => new FilterControl(); private IEnumerable beatmapSets; + public IEnumerable BeatmapSets { get { return beatmapSets; } @@ -48,7 +51,7 @@ namespace osu.Game.Overlays { if (beatmapSets?.Equals(value) ?? false) return; - beatmapSets = value; + beatmapSets = value?.ToList(); if (beatmapSets == null) return; @@ -63,14 +66,11 @@ namespace osu.Game.Overlays } ResultAmounts = new ResultCounts(distinctCount(artists), distinctCount(songs), distinctCount(tags)); - - if (beatmapSets.Any() && panels == null) - // real use case? currently only seems to be for test case - recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); } } private ResultCounts resultAmounts; + public ResultCounts ResultAmounts { get { return resultAmounts; } @@ -117,7 +117,23 @@ namespace osu.Game.Overlays }, }; - Filter.Search.Current.ValueChanged += text => { if (text != string.Empty) Header.Tabs.Current.Value = DirectTab.Search; }; + Filter.Search.Current.ValueChanged += text => + { + if (text != string.Empty) + { + Header.Tabs.Current.Value = DirectTab.Search; + + if (Filter.Tabs.Current.Value == DirectSortCriteria.Ranked) + Filter.Tabs.Current.Value = DirectSortCriteria.Relevance; + } + else + { + Header.Tabs.Current.Value = DirectTab.NewestMaps; + + if (Filter.Tabs.Current.Value == DirectSortCriteria.Relevance) + Filter.Tabs.Current.Value = DirectSortCriteria.Ranked; + } + }; ((FilterControl)Filter).Ruleset.ValueChanged += ruleset => Scheduler.AddOnce(updateSearch); Filter.DisplayStyleControl.DisplayStyle.ValueChanged += recreatePanels; Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += rankStatus => Scheduler.AddOnce(updateSearch); @@ -201,6 +217,12 @@ namespace osu.Game.Overlays panels.FadeOut(200); panels.Expire(); panels = null; + + if (playing != null) + { + playing.PreviewPlaying.Value = false; + playing = null; + } } if (BeatmapSets == null) return; @@ -216,7 +238,11 @@ namespace osu.Game.Overlays switch (displayStyle) { case PanelDisplayStyle.Grid: - return new DirectGridPanel(b) { Width = 400 }; + return new DirectGridPanel(b) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }; default: return new DirectListPanel(b); } @@ -227,10 +253,21 @@ namespace osu.Game.Overlays { if (panels != null) ScrollFlow.Remove(panels); ScrollFlow.Add(panels = newPanels); + + foreach (DirectPanel panel in p.Children) + panel.PreviewPlaying.ValueChanged += newValue => + { + if (newValue) + { + if (playing != null && playing != panel) + playing.PreviewPlaying.Value = false; + playing = panel; + } + }; }); } - private GetBeatmapSetsRequest getSetsRequest; + private SearchBeatmapSetsRequest getSetsRequest; private readonly Bindable currentQuery = new Bindable(); @@ -251,18 +288,26 @@ namespace osu.Game.Overlays 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 = new SearchBeatmapSetsRequest(currentQuery.Value ?? string.Empty, + ((FilterControl)Filter).Ruleset.Value, + Filter.DisplayStyleControl.Dropdown.Current.Value, + Filter.Tabs.Current.Value); //todo: sort direction (?) - getSetsRequest.Success += r => + getSetsRequest.Success += response => { - BeatmapSets = r?. - Select(response => response.ToBeatmapSet(rulesets)). - Where(b => beatmaps.QueryBeatmapSet(q => q.OnlineBeatmapSetID == b.OnlineBeatmapSetID) == null); + Task.Run(() => + { + var onlineIds = response.Select(r => r.OnlineBeatmapSetID).ToList(); + var presentOnlineIds = beatmaps.QueryBeatmapSets(s => onlineIds.Contains(s.OnlineBeatmapSetID)).Select(r => r.OnlineBeatmapSetID).ToList(); + var sets = response.Select(r => r.ToBeatmapSet(rulesets)).Where(b => !presentOnlineIds.Contains(b.OnlineBeatmapSetID)).ToList(); - recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); + // may not need scheduling; loads async internally. + Schedule(() => + { + BeatmapSets = sets; + recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); + }); + }); }; api.Queue(getSetsRequest); diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs index 8ebd4ac545..4a7e4f4e6e 100644 --- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.KeyBinding public override FontAwesome Icon => FontAwesome.fa_osu_hot; public override string Header => "Global"; - public GlobalKeyBindingsSection(KeyBindingInputManager manager) + public GlobalKeyBindingsSection(KeyBindingContainer manager) { Add(new DefaultBindingsSubsection(manager)); } @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.KeyBinding { protected override string Header => string.Empty; - public DefaultBindingsSubsection(KeyBindingInputManager manager) + public DefaultBindingsSubsection(KeyBindingContainer manager) : base(null) { Defaults = manager.DefaultKeyBindings; diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 4141a502a0..509a4f3856 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -19,7 +19,7 @@ using OpenTK.Input; namespace osu.Game.Overlays.KeyBinding { - internal class KeyBindingRow : Container, IFilterable + public class KeyBindingRow : Container, IFilterable { private readonly object action; private readonly IEnumerable bindings; diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs index bd69403831..c670cc0153 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.KeyBinding this.variant = variant; FlowContent.Spacing = new Vector2(0, 1); + FlowContent.Padding = new MarginPadding { Left = SettingsOverlay.CONTENT_MARGINS, Right = SettingsOverlay.CONTENT_MARGINS }; } [BackgroundDependencyLoader] @@ -37,8 +38,10 @@ namespace osu.Game.Overlays.KeyBinding foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { + int intKey = (int)defaultGroup.Key; + // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals((int)defaultGroup.Key))) + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => ((int)b.Action).Equals(intKey))) { AllowMainMouseButtons = Ruleset != null, Defaults = defaultGroup.Select(d => d.KeyCombination) @@ -52,7 +55,7 @@ namespace osu.Game.Overlays.KeyBinding } } - internal class ResetButton : OsuButton + public class ResetButton : TriangleButton { [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Overlays/KeyBindingOverlay.cs b/osu.Game/Overlays/KeyBindingOverlay.cs index 72c653030c..4394d0fec0 100644 --- a/osu.Game/Overlays/KeyBindingOverlay.cs +++ b/osu.Game/Overlays/KeyBindingOverlay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays { AddSection(new GlobalKeyBindingsSection(global)); - foreach (var ruleset in rulesets.AllRulesets) + foreach (var ruleset in rulesets.AvailableRulesets) AddSection(new RulesetBindingsSection(ruleset)); } diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index 58b259fcbb..fe3a846eb2 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Overlays.Settings.Sections.General; using OpenTK.Graphics; @@ -12,7 +13,7 @@ using osu.Game.Graphics.Cursor; namespace osu.Game.Overlays { - internal class LoginOverlay : OsuFocusedOverlayContainer + public class LoginOverlay : OsuFocusedOverlayContainer { private LoginSettings settingsSection; @@ -28,35 +29,43 @@ namespace osu.Game.Overlays { Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f, - }, new OsuContextMenuContainer { Width = 360, AutoSizeAxes = Axes.Y, - Masking = true, - AutoSizeDuration = transition_time, - AutoSizeEasing = Easing.OutQuint, Children = new Drawable[] { - settingsSection = new LoginSettings - { - Padding = new MarginPadding(10), - RequestHide = Hide, - }, new Box { - RelativeSizeAxes = Axes.X, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Height = 3, - Colour = colours.Yellow, - Alpha = 1, + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.6f, }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Masking = true, + AutoSizeDuration = transition_time, + AutoSizeEasing = Easing.OutQuint, + Children = new Drawable[] + { + settingsSection = new LoginSettings + { + Padding = new MarginPadding(10), + RequestHide = Hide, + }, + new Box + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Height = 3, + Colour = colours.Yellow, + Alpha = 1, + }, + } + } } } }; diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 880b607e78..776f2ffadb 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -159,7 +159,7 @@ namespace osu.Game.Overlays { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Both, }); } diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index 3ac8af7b2b..53d77dab6c 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -27,13 +27,13 @@ namespace osu.Game.Overlays.MedalSplash 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); + FillFlowContainer infoFlow; Children = new Drawable[] { medalContainer = new Container @@ -90,6 +90,7 @@ namespace osu.Game.Overlays.MedalSplash }, description = new TextFlowContainer { + TextAnchor = Anchor.TopCentre, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, @@ -107,6 +108,12 @@ namespace osu.Game.Overlays.MedalSplash s.Origin = Anchor.TopCentre; s.TextSize = 16; }); + + medalContainer.OnLoadComplete = d => + { + unlocked.Position = new Vector2(0f, medalContainer.DrawSize.Y / 2 + 10); + infoFlow.Position = new Vector2(0f, unlocked.Position.Y + 90); + }; } [BackgroundDependencyLoader] @@ -115,14 +122,12 @@ namespace osu.Game.Overlays.MedalSplash 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(); } @@ -172,6 +177,7 @@ namespace osu.Game.Overlays.MedalSplash this.ScaleTo(scale_when_full, duration, Easing.OutExpo); this.MoveToY(MedalOverlay.DISC_SIZE / 2 - 60, duration, Easing.OutExpo); + unlocked.Show(); name.FadeInFromZero(duration + 100); description.FadeInFromZero(duration * 2); break; diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 6bfe70d873..77b7c3add2 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI; using System; using System.Linq; using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Mods { @@ -148,7 +149,7 @@ namespace osu.Game.Overlays.Mods // the mods from Mod, only multiple if Mod is a MultiMod - public override Mod SelectedMod => Mods.ElementAtOrDefault(SelectedIndex); + public virtual Mod SelectedMod => Mods.ElementAtOrDefault(SelectedIndex); [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -207,14 +208,12 @@ namespace osu.Game.Overlays.Mods { Origin = Anchor.BottomRight, Anchor = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, Position = new Vector2(1.5f), }, foregroundIcon = new ModIcon(Mods[0]) { Origin = Anchor.BottomRight, Anchor = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, Position = new Vector2(-1.5f), }, }); @@ -225,7 +224,6 @@ namespace osu.Game.Overlays.Mods { Origin = Anchor.Centre, Anchor = Anchor.Centre, - AutoSizeAxes = Axes.Both, }); } } @@ -256,6 +254,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, TextSize = 18, }, + new HoverClickSounds() }; Mod = mod; diff --git a/osu.Game/Overlays/Mods/ModButtonEmpty.cs b/osu.Game/Overlays/Mods/ModButtonEmpty.cs index 638c2a0e47..f776c174d2 100644 --- a/osu.Game/Overlays/Mods/ModButtonEmpty.cs +++ b/osu.Game/Overlays/Mods/ModButtonEmpty.cs @@ -3,7 +3,6 @@ using OpenTK; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { @@ -12,8 +11,6 @@ namespace osu.Game.Overlays.Mods /// public class ModButtonEmpty : Container { - public virtual Mod SelectedMod => null; - public ModButtonEmpty() { Size = new Vector2(100f); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index eb643f390f..9639907914 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -17,22 +17,23 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Mods { public class ModSelectOverlay : WaveOverlayContainer { - private const int button_duration = 700; - private const int ranked_multiplier_duration = 700; private const float content_width = 0.8f; - private Color4 lowMultiplierColour, highMultiplierColour; + protected Color4 LowMultiplierColour, HighMultiplierColour; - private readonly OsuSpriteText rankedLabel; - private readonly OsuSpriteText multiplierLabel; - private readonly FillFlowContainer rankedMultiplerContainer; + protected readonly TriangleButton DeselectAllButton; + protected readonly OsuSpriteText MultiplierLabel; + private readonly FillFlowContainer footerContainer; - private readonly FillFlowContainer modSectionsContainer; + protected override bool BlockPassThroughKeyboard => false; + + protected readonly FillFlowContainer ModSectionsContainer; public readonly Bindable> SelectedMods = new Bindable>(); @@ -42,7 +43,7 @@ namespace osu.Game.Overlays.Mods { var instance = newRuleset.CreateInstance(); - foreach (ModSection section in modSectionsContainer.Children) + foreach (ModSection section in ModSectionsContainer.Children) section.Mods = instance.GetModsFor(section.ModType); refreshSelectedMods(); } @@ -50,13 +51,13 @@ namespace osu.Game.Overlays.Mods [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuColour colours, OsuGame osu, RulesetStore rulesets) { - lowMultiplierColour = colours.Red; - highMultiplierColour = colours.Green; + LowMultiplierColour = colours.Red; + HighMultiplierColour = colours.Green; if (osu != null) Ruleset.BindTo(osu.Ruleset); else - Ruleset.Value = rulesets.AllRulesets.First(); + Ruleset.Value = rulesets.AvailableRulesets.First(); Ruleset.ValueChanged += rulesetChanged; Ruleset.TriggerChange(); @@ -66,14 +67,14 @@ namespace osu.Game.Overlays.Mods { base.PopOut(); - rankedMultiplerContainer.MoveToX(rankedMultiplerContainer.DrawSize.X, APPEAR_DURATION, Easing.InSine); - rankedMultiplerContainer.FadeOut(APPEAR_DURATION, Easing.InSine); + footerContainer.MoveToX(footerContainer.DrawSize.X, DISAPPEAR_DURATION, Easing.InSine); + footerContainer.FadeOut(DISAPPEAR_DURATION, Easing.InSine); - foreach (ModSection section in modSectionsContainer.Children) + foreach (ModSection section in ModSectionsContainer.Children) { - 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); + section.ButtonsContainer.TransformSpacingTo(new Vector2(100f, 0f), DISAPPEAR_DURATION, Easing.InSine); + section.ButtonsContainer.MoveToX(100f, DISAPPEAR_DURATION, Easing.InSine); + section.ButtonsContainer.FadeOut(DISAPPEAR_DURATION, Easing.InSine); } } @@ -81,27 +82,28 @@ namespace osu.Game.Overlays.Mods { base.PopIn(); - rankedMultiplerContainer.MoveToX(0, ranked_multiplier_duration, Easing.OutQuint); - rankedMultiplerContainer.FadeIn(ranked_multiplier_duration, Easing.OutQuint); + footerContainer.MoveToX(0, APPEAR_DURATION, Easing.OutQuint); + footerContainer.FadeIn(APPEAR_DURATION, Easing.OutQuint); - foreach (ModSection section in modSectionsContainer.Children) + foreach (ModSection section in ModSectionsContainer.Children) { - 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); + section.ButtonsContainer.TransformSpacingTo(new Vector2(50f, 0f), APPEAR_DURATION, Easing.OutQuint); + section.ButtonsContainer.MoveToX(0, APPEAR_DURATION, Easing.OutQuint); + section.ButtonsContainer.FadeIn(APPEAR_DURATION, Easing.OutQuint); } } public void DeselectAll() { - foreach (ModSection section in modSectionsContainer.Children) + foreach (ModSection section in ModSectionsContainer.Children) section.DeselectAll(); + refreshSelectedMods(); } public void DeselectTypes(Type[] modTypes) { if (modTypes.Length == 0) return; - foreach (ModSection section in modSectionsContainer.Children) + foreach (ModSection section in ModSectionsContainer.Children) section.DeselectTypes(modTypes); } @@ -114,7 +116,7 @@ namespace osu.Game.Overlays.Mods private void refreshSelectedMods() { - SelectedMods.Value = modSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); + SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); double multiplier = 1.0; bool ranked = true; @@ -129,16 +131,16 @@ namespace osu.Game.Overlays.Mods // 1.05x // 1.20x - multiplierLabel.Text = $"{multiplier:N2}x"; - string rankedString = ranked ? "Ranked" : "Unranked"; - rankedLabel.Text = $@"{rankedString}, Score Multiplier: "; + MultiplierLabel.Text = $"{multiplier:N2}x"; + if (!ranked) + MultiplierLabel.Text += " (Unranked)"; if (multiplier > 1.0) - multiplierLabel.FadeColour(highMultiplierColour, 200); + MultiplierLabel.FadeColour(HighMultiplierColour, 200); else if (multiplier < 1.0) - multiplierLabel.FadeColour(lowMultiplierColour, 200); + MultiplierLabel.FadeColour(LowMultiplierColour, 200); else - multiplierLabel.FadeColour(Color4.White, 200); + MultiplierLabel.FadeColour(Color4.White, 200); } public ModSelectOverlay() @@ -232,7 +234,7 @@ namespace osu.Game.Overlays.Mods }, new OsuSpriteText { - Text = @"Others are just for fun", + Text = @"Others are just for fun.", TextSize = 18, Shadow = true, }, @@ -241,7 +243,7 @@ namespace osu.Game.Overlays.Mods }, }, // Body - modSectionsContainer = new FillFlowContainer + ModSectionsContainer = new FillFlowContainer { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, @@ -289,7 +291,7 @@ namespace osu.Game.Overlays.Mods Colour = new Color4(172, 20, 116, 255), Alpha = 0.5f, }, - rankedMultiplerContainer = new FillFlowContainer + footerContainer = new FillFlowContainer { Origin = Anchor.BottomCentre, Anchor = Anchor.BottomCentre, @@ -299,26 +301,42 @@ namespace osu.Game.Overlays.Mods Direction = FillDirection.Horizontal, Padding = new MarginPadding { - Top = 20, - Bottom = 20, + Vertical = 15 }, Children = new Drawable[] { - rankedLabel = new OsuSpriteText + DeselectAllButton = new TriangleButton { - Text = @"Ranked, Score Multiplier: ", + Width = 180, + Text = "Deselect All", + Action = DeselectAll, + Margin = new MarginPadding + { + Right = 20 + } + }, + new OsuSpriteText + { + Text = @"Score Multiplier: ", TextSize = 30, Shadow = true, + Margin = new MarginPadding + { + Top = 5 + } }, - multiplierLabel = new OsuSpriteText + MultiplierLabel = new OsuSpriteText { Font = @"Exo2.0-Bold", - Text = @"1.00x", TextSize = 30, Shadow = true, - }, - }, - }, + Margin = new MarginPadding + { + Top = 5 + } + } + } + } }, }, }, diff --git a/osu.Game/Overlays/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs index 56cd6e864b..52d311e501 100644 --- a/osu.Game/Overlays/Music/FilterControl.cs +++ b/osu.Game/Overlays/Music/FilterControl.cs @@ -13,7 +13,7 @@ using System; namespace osu.Game.Overlays.Music { - internal class FilterControl : Container + public class FilterControl : Container { public readonly FilterTextBox Search; diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 723b3f4e96..8168929f9c 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -17,7 +17,7 @@ using OpenTK; namespace osu.Game.Overlays.Music { - internal class PlaylistItem : Container, IFilterable, IDraggable + public class PlaylistItem : Container, IFilterable, IDraggable { private const float fade_duration = 100; diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 6f1eaded7f..245b2d36ce 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -14,7 +14,7 @@ using OpenTK; namespace osu.Game.Overlays.Music { - internal class PlaylistList : CompositeDrawable + public class PlaylistList : CompositeDrawable { public Action OnSelect; @@ -35,7 +35,11 @@ namespace osu.Game.Overlays.Music set { base.Padding = value; } } - public IEnumerable BeatmapSets { set { items.Sets = value; } } + public IEnumerable BeatmapSets + { + get { return items.Sets; } + set { items.Sets = value; } + } public BeatmapSetInfo FirstVisibleSet => items.FirstVisibleSet; public BeatmapSetInfo NextSet => items.NextSet; @@ -48,7 +52,7 @@ namespace osu.Game.Overlays.Music } public void AddBeatmapSet(BeatmapSetInfo beatmapSet) => items.AddBeatmapSet(beatmapSet); - public bool RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => items.RemoveBeatmapSet(beatmapSet); + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => items.RemoveBeatmapSet(beatmapSet); public void Filter(string searchTerm) => items.SearchTerm = searchTerm; @@ -81,6 +85,7 @@ namespace osu.Game.Overlays.Music public IEnumerable Sets { + get { return items.Select(x => x.BeatmapSetInfo).ToList(); } set { items.Clear(); @@ -103,12 +108,11 @@ namespace osu.Game.Overlays.Music }); } - public bool RemoveBeatmapSet(BeatmapSetInfo beatmapSet) + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) { var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == beatmapSet.ID); - if (itemToRemove == null) - return false; - return items.Remove(itemToRemove); + if (itemToRemove != null) + items.Remove(itemToRemove); } public BeatmapSetInfo SelectedSet @@ -230,6 +234,7 @@ namespace osu.Game.Overlays.Music private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren { public IEnumerable FilterTerms => new string[] { }; + public bool MatchingFilter { set diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index d05ad85726..d913895159 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Music private readonly Bindable beatmapBacking = new Bindable(); - public IEnumerable BeatmapSets; + public IEnumerable BeatmapSets => list.BeatmapSets; [BackgroundDependencyLoader] private void load(OsuGameBase game, BeatmapManager beatmaps, OsuColour colours) @@ -74,11 +74,10 @@ namespace osu.Game.Overlays.Music }, }; - beatmaps.BeatmapSetAdded += s => Schedule(() => list.AddBeatmapSet(s)); - beatmaps.BeatmapSetRemoved += s => Schedule(() => list.RemoveBeatmapSet(s)); - - list.BeatmapSets = BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); + beatmaps.BeatmapSetAdded += list.AddBeatmapSet; + beatmaps.BeatmapSetRemoved += list.RemoveBeatmapSet; + list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); beatmapBacking.BindTo(game.Beatmap); @@ -121,7 +120,7 @@ namespace osu.Game.Overlays.Music return; } - playSpecified(set.Beatmaps[0]); + playSpecified(set.Beatmaps.First()); } public void PlayPrevious() @@ -130,7 +129,7 @@ namespace osu.Game.Overlays.Music if (playable != null) { - playSpecified(playable.Beatmaps[0]); + playSpecified(playable.Beatmaps.First()); list.SelectedSet = playable; } } @@ -141,7 +140,7 @@ namespace osu.Game.Overlays.Music if (playable != null) { - playSpecified(playable.Beatmaps[0]); + playSpecified(playable.Beatmaps.First()); list.SelectedSet = playable; } } @@ -149,7 +148,10 @@ namespace osu.Game.Overlays.Music private void playSpecified(BeatmapInfo info) { beatmapBacking.Value = beatmaps.GetWorkingBeatmap(info, beatmapBacking); - beatmapBacking.Value.Track.Start(); + + var track = beatmapBacking.Value.Track; + + track.Restart(); } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 64d0d628f0..b8f33c9a60 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays private const float bottom_black_area_height = 55; - private Drawable currentBackground; + private Drawable background; private ProgressBar progressBar; private IconButton prevButton; @@ -120,7 +120,7 @@ namespace osu.Game.Overlays }, Children = new[] { - currentBackground = new Background(), + background = new Background(), title = new OsuSpriteText { Origin = Anchor.BottomCentre, @@ -161,11 +161,15 @@ namespace osu.Game.Overlays { prevButton = new IconButton { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Action = prev, Icon = FontAwesome.fa_step_backward, }, playButton = new IconButton { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Scale = new Vector2(1.4f), IconScale = new Vector2(1.4f), Action = play, @@ -173,6 +177,8 @@ namespace osu.Game.Overlays }, nextButton = new IconButton { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Action = next, Icon = FontAwesome.fa_step_forward, }, @@ -245,7 +251,7 @@ namespace osu.Game.Overlays playButton.Icon = track.IsRunning ? FontAwesome.fa_pause_circle_o : FontAwesome.fa_play_circle_o; - if (track.HasCompleted && !track.Looping && !beatmapBacking.Disabled) + if (track.HasCompleted && !track.Looping && !beatmapBacking.Disabled && playlist.BeatmapSets.Any()) next(); } else @@ -328,6 +334,7 @@ namespace osu.Game.Overlays pendingBeatmapSwitch = Schedule(delegate { + // todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync() Task.Run(() => { if (beatmap?.Beatmap == null) //this is not needed if a placeholder exists @@ -346,29 +353,26 @@ namespace osu.Game.Overlays } }); - playerContainer.Add(new AsyncLoadWrapper(new Background(beatmap) + LoadComponentAsync(new Background(beatmap) { Depth = float.MaxValue }, newBackground => { - OnLoadComplete = newBackground => + switch (direction) { - switch (direction) - { - case TransformDirection.Next: - newBackground.Position = new Vector2(400, 0); - newBackground.MoveToX(0, 500, Easing.OutCubic); - currentBackground.MoveToX(-400, 500, Easing.OutCubic); - break; - case TransformDirection.Prev: - newBackground.Position = new Vector2(-400, 0); - newBackground.MoveToX(0, 500, Easing.OutCubic); - currentBackground.MoveToX(400, 500, Easing.OutCubic); - break; - } - currentBackground.Expire(); - currentBackground = newBackground; + case TransformDirection.Next: + newBackground.Position = new Vector2(400, 0); + newBackground.MoveToX(0, 500, Easing.OutCubic); + background.MoveToX(-400, 500, Easing.OutCubic); + break; + case TransformDirection.Prev: + newBackground.Position = new Vector2(-400, 0); + newBackground.MoveToX(0, 500, Easing.OutCubic); + background.MoveToX(400, 500, Easing.OutCubic); + break; } - }) - { - Depth = float.MaxValue, + + background.Expire(); + background = newBackground; + + playerContainer.Add(newBackground); }); }); } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index a31291e1b8..12c7fe64ba 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -22,13 +22,12 @@ namespace osu.Game.Overlays.Notifications } } + public string CompletionText { get; set; } = "Task has completed!"; + public float Progress { get { return progressBar.Progress; } - set - { - progressBar.Progress = value; - } + set { Schedule(() => progressBar.Progress = value); } } protected override void LoadComplete() @@ -44,41 +43,44 @@ namespace osu.Game.Overlays.Notifications get { return state; } set { - bool stateChanged = state != value; - state = value; - - if (IsLoaded) + Schedule(() => { - switch (state) - { - case ProgressNotificationState.Queued: - Light.Colour = colourQueued; - Light.Pulsate = false; - progressBar.Active = false; - break; - case ProgressNotificationState.Active: - Light.Colour = colourActive; - Light.Pulsate = true; - progressBar.Active = true; - break; - case ProgressNotificationState.Cancelled: - Light.Colour = colourCancelled; - Light.Pulsate = false; - progressBar.Active = false; - break; - } - } + bool stateChanged = state != value; + state = value; - if (stateChanged) - { - switch (state) + if (IsLoaded) { - case ProgressNotificationState.Completed: - NotificationContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); - this.FadeOut(200).Finally(d => Completed()); - break; + switch (state) + { + case ProgressNotificationState.Queued: + Light.Colour = colourQueued; + Light.Pulsate = false; + progressBar.Active = false; + break; + case ProgressNotificationState.Active: + Light.Colour = colourActive; + Light.Pulsate = true; + progressBar.Active = true; + break; + case ProgressNotificationState.Cancelled: + Light.Colour = colourCancelled; + Light.Pulsate = false; + progressBar.Active = false; + break; + } } - } + + if (stateChanged) + { + switch (state) + { + case ProgressNotificationState.Completed: + NotificationContent.MoveToY(-DrawSize.Y / 2, 200, Easing.OutQuint); + this.FadeOut(200).Finally(d => Completed()); + break; + } + } + }); } } @@ -87,7 +89,7 @@ namespace osu.Game.Overlays.Notifications protected virtual Notification CreateCompletionNotification() => new ProgressCompletionNotification { Activated = CompletionClickAction, - Text = "Task has completed!" + Text = CompletionText }; protected virtual void Completed() @@ -232,4 +234,4 @@ namespace osu.Game.Overlays.Notifications Completed, Cancelled } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index d6a86fe714..a706799664 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -12,13 +12,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Users; using System.Diagnostics; -using System.Globalization; +using System.Collections.Generic; +using osu.Framework.Graphics.Cursor; namespace osu.Game.Overlays.Profile { @@ -27,8 +27,9 @@ namespace osu.Game.Overlays.Profile private readonly OsuTextFlowContainer infoTextLeft; private readonly LinkFlowContainer infoTextRight; private readonly FillFlowContainer scoreText, scoreNumberText; + private readonly RankGraph rankGraph; - private readonly Container coverContainer, chartContainer, supporterTag; + private readonly Container coverContainer, supporterTag; private readonly Sprite levelBadge; private readonly SpriteText levelText; private readonly GradeBadge gradeSSPlus, gradeSS, gradeSPlus, gradeS, gradeA; @@ -53,6 +54,7 @@ namespace osu.Game.Overlays.Profile { RelativeSizeAxes = Axes.X, Height = cover_height, + Masking = true, Children = new Drawable[] { new Box @@ -119,15 +121,11 @@ namespace osu.Game.Overlays.Profile } } }, - new LinkFlowContainer.LinkText + new LinkFlowContainer.ProfileLink(user) { - Text = user.Username, - Url = $@"https://osu.ppy.sh/users/{user.Id}", - TextSize = 30, - Font = @"Exo2.0-RegularItalic", Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Y = -48 + Y = -48, }, countryFlag = new DrawableFlag(user.Country?.FlagName) { @@ -276,7 +274,7 @@ namespace osu.Game.Overlays.Profile } } }, - chartContainer = new Container + new Container { RelativeSizeAxes = Axes.X, Anchor = Anchor.BottomCentre, @@ -288,6 +286,10 @@ namespace osu.Game.Overlays.Profile { Colour = Color4.Black.Opacity(0.25f), RelativeSizeAxes = Axes.Both + }, + rankGraph = new RankGraph + { + RelativeSizeAxes = Axes.Both } } } @@ -306,11 +308,7 @@ namespace osu.Game.Overlays.Profile public User User { - get - { - return user; - } - + get { return user; } set { user = value; @@ -320,19 +318,15 @@ namespace osu.Game.Overlays.Profile private void loadUser() { - coverContainer.Add(new AsyncLoadWrapper(new UserCoverBackground(user) + LoadComponentAsync(new UserCoverBackground(user) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, FillMode = FillMode.Fill, - OnLoadComplete = d => d.FadeInFromZero(200) - }) - { - Masking = true, - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue - }); + OnLoadComplete = d => d.FadeInFromZero(200), + Depth = float.MaxValue, + }, coverContainer.Add); if (user.IsSupporter) supporterTag.Show(); @@ -389,7 +383,7 @@ namespace osu.Game.Overlays.Profile } tryAddInfoRightLine(FontAwesome.fa_map_marker, user.Location); - tryAddInfoRightLine(FontAwesome.fa_heart_o, user.Intrerests); + tryAddInfoRightLine(FontAwesome.fa_heart_o, user.Interests); tryAddInfoRightLine(FontAwesome.fa_suitcase, user.Occupation); infoTextRight.NewParagraph(); if (!string.IsNullOrEmpty(user.Twitter)) @@ -405,7 +399,7 @@ namespace osu.Game.Overlays.Profile scoreText.Add(createScoreText("Ranked Score")); scoreNumberText.Add(createScoreNumberText(user.Statistics.RankedScore.ToString(@"#,0"))); scoreText.Add(createScoreText("Accuracy")); - scoreNumberText.Add(createScoreNumberText($"{user.Statistics.Accuracy.ToString("0.##", CultureInfo.CurrentCulture)}%")); + scoreNumberText.Add(createScoreNumberText($"{user.Statistics.Accuracy:0.##}%")); scoreText.Add(createScoreText("Play Count")); scoreNumberText.Add(createScoreNumberText(user.Statistics.PlayCount.ToString(@"#,0"))); scoreText.Add(createScoreText("Total Score")); @@ -427,7 +421,7 @@ namespace osu.Game.Overlays.Profile gradeSPlus.DisplayCount = 0; gradeSSPlus.DisplayCount = 0; - chartContainer.Add(new RankChart(user) { RelativeSizeAxes = Axes.Both }); + rankGraph.User.Value = user; } } @@ -509,34 +503,42 @@ namespace osu.Game.Overlays.Profile public class LinkText : OsuSpriteText { - public override bool HandleInput => Url != null; + private readonly OsuHoverContainer content; - public string Url; + public override bool HandleInput => content.Action != null; - private Color4 hoverColour; + protected override Container Content => content ?? (Container)this; - protected override bool OnHover(InputState state) + protected override IEnumerable FlowingChildren => Children; + + public string Url { - this.FadeColour(hoverColour, 500, Easing.OutQuint); - return base.OnHover(state); + set + { + if (value != null) + content.Action = () => Process.Start(value); + } } - protected override void OnHoverLost(InputState state) + public LinkText() { - this.FadeColour(Color4.White, 500, Easing.OutQuint); - base.OnHoverLost(state); + AddInternal(content = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + }); } + } - protected override bool OnClick(InputState state) - { - Process.Start(Url); - return true; - } + public class ProfileLink : LinkText, IHasTooltip + { + public string TooltipText => "View Profile in Browser"; - [BackgroundDependencyLoader] - private void load(OsuColour colours) + public ProfileLink(User user) { - hoverColour = colours.Yellow; + Text = user.Username; + Url = $@"https://osu.ppy.sh/users/{user.Id}"; + Font = @"Exo2.0-RegularItalic"; + TextSize = 30; } } } diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs index 2b5084e321..99178570a6 100644 --- a/osu.Game/Overlays/Profile/ProfileSection.cs +++ b/osu.Game/Overlays/Profile/ProfileSection.cs @@ -7,7 +7,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Users; using OpenTK.Graphics; +using osu.Framework.Configuration; namespace osu.Game.Overlays.Profile { @@ -18,8 +20,11 @@ namespace osu.Game.Overlays.Profile public abstract string Identifier { get; } private readonly FillFlowContainer content; + protected override Container Content => content; + public readonly Bindable User = new Bindable(); + protected ProfileSection() { Direction = FillDirection.Vertical; diff --git a/osu.Game/Overlays/Profile/RankChart.cs b/osu.Game/Overlays/Profile/RankChart.cs deleted file mode 100644 index 2e2286098a..0000000000 --- a/osu.Game/Overlays/Profile/RankChart.cs +++ /dev/null @@ -1,177 +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 osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Users; - -namespace osu.Game.Overlays.Profile -{ - public class RankChart : Container - { - private readonly SpriteText rankText, performanceText, relativeText; - private readonly RankChartLineGraph graph; - - private readonly int[] ranks; - - private const float primary_textsize = 25, secondary_textsize = 13, padding = 10; - - private readonly User user; - - public RankChart(User user) - { - this.user = user; - - Padding = new MarginPadding { Vertical = padding }; - Children = new Drawable[] - { - rankText = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Font = @"Exo2.0-RegularItalic", - TextSize = primary_textsize - }, - relativeText = new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Font = @"Exo2.0-RegularItalic", - Y = 25, - TextSize = secondary_textsize - }, - performanceText = new OsuSpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Font = @"Exo2.0-RegularItalic", - TextSize = secondary_textsize - }, - graph = new RankChartLineGraph - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Y = -secondary_textsize, - DefaultValueCount = 90, - BallRelease = updateRankTexts, - BallMove = showHistoryRankTexts - } - }; - - ranks = user.RankHistory?.Data ?? new[] { user.Statistics.Rank }; - } - - private void updateRankTexts() - { - rankText.Text = user.Statistics.Rank > 0 ? $"#{user.Statistics.Rank:#,0}" : "no rank"; - performanceText.Text = user.Statistics.PP != null ? $"{user.Statistics.PP:#,0}pp" : string.Empty; - relativeText.Text = $"{user.Country?.FullName} #{user.CountryRank:#,0}"; - } - - private void showHistoryRankTexts(int dayIndex) - { - rankText.Text = ranks[dayIndex] > 0 ? $"#{ranks[dayIndex]:#,0}" : "no rank"; - relativeText.Text = dayIndex == ranks.Length ? "Now" : $"{ranks.Length - dayIndex} days ago"; - //plural should be handled in a general way - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - graph.Colour = colours.Yellow; - - if (user.Statistics.Rank > 0) - { - // use logarithmic coordinates - graph.Values = ranks.Select(x => -(float)Math.Log(x)); - graph.ResetBall(); - } - } - - public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true) - { - if ((invalidation & Invalidation.DrawSize) != 0) - { - graph.Height = DrawHeight - padding * 2 - primary_textsize - secondary_textsize * 2; - } - - return base.Invalidate(invalidation, source, shallPropagate); - } - - private class RankChartLineGraph : LineGraph - { - private readonly CircularContainer ball; - private bool ballShown; - - private const double transform_duration = 100; - - public Action BallMove; - public Action BallRelease; - - public RankChartLineGraph() - { - Add(ball = new CircularContainer - { - Size = new Vector2(8), - Masking = true, - Origin = Anchor.Centre, - Alpha = 0, - RelativePositionAxes = Axes.Both, - Children = new Drawable[] - { - new Box { RelativeSizeAxes = Axes.Both } - } - }); - } - - public void ResetBall() - { - ball.MoveTo(new Vector2(1, GetYPosition(Values.Last())), ballShown ? transform_duration : 0, Easing.OutQuint); - ball.Show(); - BallRelease(); - ballShown = true; - } - - protected override bool OnMouseMove(InputState state) - { - if (ballShown) - { - var values = (IList)Values; - var position = ToLocalSpace(state.Mouse.NativeState.Position); - int count = Math.Max(values.Count, DefaultValueCount); - int index = (int)Math.Round(position.X / DrawWidth * (count - 1)); - if (index >= count - values.Count) - { - int i = index + values.Count - count; - float y = GetYPosition(values[i]); - if (Math.Abs(y * DrawHeight - position.Y) <= 8f) - { - ball.MoveTo(new Vector2(index / (float)(count - 1), y), transform_duration, Easing.OutQuint); - BallMove(i); - } - } - } - return base.OnMouseMove(state); - } - - protected override void OnHoverLost(InputState state) - { - if (ballShown) - ResetBall(); - base.OnHoverLost(state); - } - } - } -} diff --git a/osu.Game/Overlays/Profile/RankGraph.cs b/osu.Game/Overlays/Profile/RankGraph.cs new file mode 100644 index 0000000000..d216789b1a --- /dev/null +++ b/osu.Game/Overlays/Profile/RankGraph.cs @@ -0,0 +1,217 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using OpenTK; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Users; +using System.Collections.Generic; +using osu.Framework.Configuration; + +namespace osu.Game.Overlays.Profile +{ + public class RankGraph : Container + { + private const float primary_textsize = 25; + private const float secondary_textsize = 13; + private const float padding = 10; + private const float fade_duration = 150; + private const int ranked_days = 88; + + private readonly SpriteText rankText, performanceText, relativeText; + private readonly RankChartLineGraph graph; + private readonly OsuSpriteText placeholder; + + private KeyValuePair[] ranks; + public Bindable User = new Bindable(); + + public RankGraph() + { + Padding = new MarginPadding { Vertical = padding }; + Children = new Drawable[] + { + placeholder = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "No recent plays", + TextSize = 14, + Font = @"Exo2.0-RegularItalic", + }, + rankText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = @"Exo2.0-RegularItalic", + TextSize = primary_textsize + }, + relativeText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = @"Exo2.0-RegularItalic", + Y = 25, + TextSize = secondary_textsize + }, + performanceText = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = @"Exo2.0-RegularItalic", + TextSize = secondary_textsize + }, + graph = new RankChartLineGraph + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Height = 75, + Y = -secondary_textsize, + Alpha = 0, + } + }; + + graph.OnBallMove += showHistoryRankTexts; + + User.ValueChanged += userChanged; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + graph.Colour = colours.Yellow; + } + + private void userChanged(User user) + { + placeholder.FadeIn(fade_duration, Easing.Out); + + if (user == null) + { + rankText.Text = string.Empty; + performanceText.Text = string.Empty; + relativeText.Text = string.Empty; + graph.FadeOut(fade_duration, Easing.Out); + ranks = null; + return; + } + + int[] userRanks = user.RankHistory?.Data ?? new[] { user.Statistics.Rank }; + ranks = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); + + if (ranks.Length > 1) + { + placeholder.FadeOut(fade_duration, Easing.Out); + + graph.DefaultValueCount = ranks.Length; + graph.Values = ranks.Select(x => -(float)Math.Log(x.Value)); + graph.SetStaticBallPosition(); + } + + graph.FadeTo(ranks.Length > 1 ? 1 : 0, fade_duration, Easing.Out); + + updateRankTexts(); + } + + private void updateRankTexts() + { + rankText.Text = User.Value.Statistics.Rank > 0 ? $"#{User.Value.Statistics.Rank:#,0}" : "no rank"; + performanceText.Text = User.Value.Statistics.PP != null ? $"{User.Value.Statistics.PP:#,0}pp" : string.Empty; + relativeText.Text = $"{User.Value.Country?.FullName} #{User.Value.CountryRank:#,0}"; + } + + private void showHistoryRankTexts(int dayIndex) + { + rankText.Text = $"#{ranks[dayIndex].Value:#,0}"; + relativeText.Text = dayIndex + 1 == ranks.Length ? "Now" : $"{ranked_days - ranks[dayIndex].Key} days ago"; + } + + protected override bool OnHover(InputState state) + { + if (ranks?.Length > 1) + { + graph.UpdateBallPosition(state.Mouse.Position.X); + graph.ShowBall(); + } + return base.OnHover(state); + } + + protected override bool OnMouseMove(InputState state) + { + if (ranks?.Length > 1) + graph.UpdateBallPosition(state.Mouse.Position.X); + + return base.OnMouseMove(state); + } + + protected override void OnHoverLost(InputState state) + { + if (ranks?.Length > 1) + { + graph.HideBall(); + updateRankTexts(); + } + + base.OnHoverLost(state); + } + + private class RankChartLineGraph : LineGraph + { + private readonly CircularContainer staticBall; + private readonly CircularContainer movingBall; + + public Action OnBallMove; + + public RankChartLineGraph() + { + Add(staticBall = new CircularContainer + { + Origin = Anchor.Centre, + Size = new Vector2(8), + Masking = true, + RelativePositionAxes = Axes.Both, + Child = new Box { RelativeSizeAxes = Axes.Both } + }); + Add(movingBall = new CircularContainer + { + Origin = Anchor.Centre, + Size = new Vector2(8), + Alpha = 0, + Masking = true, + RelativePositionAxes = Axes.Both, + Child = new Box { RelativeSizeAxes = Axes.Both } + }); + } + + public void SetStaticBallPosition() => staticBall.Position = new Vector2(1, GetYPosition(Values.Last())); + + public void UpdateBallPosition(float mouseXPosition) + { + int index = calculateIndex(mouseXPosition); + movingBall.Position = calculateBallPosition(index); + OnBallMove.Invoke(index); + } + + public void ShowBall() => movingBall.FadeIn(fade_duration); + + public void HideBall() => movingBall.FadeOut(fade_duration); + + private int calculateIndex(float mouseXPosition) => (int)Math.Round(mouseXPosition / DrawWidth * (DefaultValueCount - 1)); + + private Vector2 calculateBallPosition(int index) + { + float y = GetYPosition(Values.ElementAt(index)); + return new Vector2(index / (float)(DefaultValueCount - 1), y); + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs new file mode 100644 index 0000000000..bfd8db5d9c --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +namespace osu.Game.Overlays.Profile.Sections +{ + /// + /// Display artist/title/mapper information, commonly used as the left portion of a profile or score display row (see ). + /// + public class BeatmapMetadataContainer : OsuHoverContainer, IHasTooltip + { + private readonly BeatmapInfo beatmap; + + public BeatmapMetadataContainer(BeatmapInfo beatmap) + { + this.beatmap = beatmap; + AutoSizeAxes = Axes.Both; + TooltipText = $"{beatmap.Metadata.Artist} - {beatmap.Metadata.Title}"; + } + + public string TooltipText { get; } + + [BackgroundDependencyLoader(true)] + private void load(LocalisationEngine locale, BeatmapSetOverlay beatmapSetOverlay) + { + Action = () => + { + if (beatmap.OnlineBeatmapSetID.HasValue) beatmapSetOverlay?.ShowBeatmapSet(beatmap.OnlineBeatmapSetID.Value); + }; + + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Current = locale.GetUnicodePreference( + $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ", + $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] " + ), + TextSize = 15, + Font = "Exo2.0-SemiBoldItalic", + }, + new OsuSpriteText + { + Current = locale.GetUnicodePreference(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), + TextSize = 12, + Padding = new MarginPadding { Top = 3 }, + Font = "Exo2.0-RegularItalic", + }, + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs new file mode 100644 index 0000000000..4bfb8107b4 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -0,0 +1,72 @@ +// 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.Configuration; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Direct; +using osu.Game.Users; +using System.Linq; + +namespace osu.Game.Overlays.Profile.Sections.Beatmaps +{ + public class PaginatedBeatmapContainer : PaginatedContainer + { + private const float panel_padding = 10f; + + private readonly BeatmapSetType type; + + private DirectPanel currentlyPlaying; + + public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string header, string missing = "None... yet.") + : base(user, header, missing) + { + this.type = type; + + ItemsPerPage = 6; + + ItemsContainer.Spacing = new Vector2(panel_padding); + } + + protected override void ShowMore() + { + base.ShowMore(); + + var req = new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); + + req.Success += sets => + { + ShowMoreButton.FadeTo(sets.Count == ItemsPerPage ? 1 : 0); + ShowMoreLoading.Hide(); + + if (!sets.Any() && VisiblePages == 1) + { + MissingText.Show(); + return; + } + + foreach (var s in sets) + { + if (!s.OnlineBeatmapSetID.HasValue) + continue; + + var panel = new DirectGridPanel(s.ToBeatmapSet(Rulesets)); + ItemsContainer.Add(panel); + + panel.PreviewPlaying.ValueChanged += isPlaying => + { + if (!isPlaying) return; + + if (currentlyPlaying != null && currentlyPlaying != panel) + currentlyPlaying.PreviewPlaying.Value = false; + + currentlyPlaying = panel; + }; + } + }; + + Api.Queue(req); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs index 1c39223e6f..3e88795b77 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs @@ -1,6 +1,9 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Profile.Sections.Beatmaps; + namespace osu.Game.Overlays.Profile.Sections { public class BeatmapsSection : ProfileSection @@ -8,5 +11,16 @@ namespace osu.Game.Overlays.Profile.Sections public override string Title => "Beatmaps"; public override string Identifier => "beatmaps"; + + public BeatmapsSection() + { + Children = new[] + { + new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, "Favourite Beatmaps"), + new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User, "Ranked & Approved Beatmaps"), + new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, "Pending Beatmaps"), + new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, "Graveyarded Beatmaps"), + }; + } } } diff --git a/osu.Game/Overlays/Profile/Sections/DrawableProfileRow.cs b/osu.Game/Overlays/Profile/Sections/DrawableProfileRow.cs new file mode 100644 index 0000000000..0607549f20 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/DrawableProfileRow.cs @@ -0,0 +1,124 @@ +// 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.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 OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Profile.Sections +{ + public abstract class DrawableProfileRow : Container + { + private const int fade_duration = 200; + + private Box underscoreLine; + private readonly Box coloredBackground; + private readonly Container background; + + /// + /// A visual element displayed to the left of content. + /// + protected abstract Drawable CreateLeftVisual(); + + protected FillFlowContainer LeftFlowContainer { get; private set; } + protected FillFlowContainer RightFlowContainer { get; private set; } + + protected override Container Content { get; } + + protected DrawableProfileRow() + { + RelativeSizeAxes = Axes.X; + Height = 60; + InternalChildren = new Drawable[] + { + background = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 3, + Alpha = 0, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 1f), + Radius = 1f, + Colour = Color4.Black.Opacity(0.2f), + }, + Child = coloredBackground = new Box { RelativeSizeAxes = Axes.Both } + }, + Content = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 0.97f, + }, + }; + } + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colour) + { + AddRange(new Drawable[] + { + underscoreLine = new Box + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Height = 1, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + CreateLeftVisual(), + LeftFlowContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 10 }, + Direction = FillDirection.Vertical, + }, + } + }, + RightFlowContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Vertical, + }, + }); + + coloredBackground.Colour = underscoreLine.Colour = colour.Gray4; + } + + protected override bool OnClick(InputState state) => true; + + protected override bool OnHover(InputState state) + { + background.FadeIn(fade_duration, Easing.OutQuint); + underscoreLine.FadeOut(fade_duration, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(InputState state) + { + background.FadeOut(fade_duration, Easing.OutQuint); + underscoreLine.FadeIn(fade_duration, Easing.OutQuint); + base.OnHoverLost(state); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs new file mode 100644 index 0000000000..14ab5d8279 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs @@ -0,0 +1,105 @@ +// 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.Beatmaps.Drawables; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using OpenTK; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class DrawableMostPlayedRow : DrawableProfileRow + { + private readonly BeatmapInfo beatmap; + private readonly int playCount; + private OsuHoverContainer mapperContainer; + + public DrawableMostPlayedRow(BeatmapInfo beatmap, int playCount) + { + this.beatmap = beatmap; + this.playCount = playCount; + } + + protected override Drawable CreateLeftVisual() => new DelayedLoadWrapper(new BeatmapSetCover(beatmap.BeatmapSet, BeatmapSetCoverType.List) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + OnLoadComplete = d => d.FadeInFromZero(500, Easing.OutQuint) + }) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.None, + Size = new Vector2(80, 50), + }; + + [BackgroundDependencyLoader(true)] + private void load(UserProfileOverlay profileOverlay) + { + LeftFlowContainer.Add(new BeatmapMetadataContainer(beatmap)); + LeftFlowContainer.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = @"mapped by ", + TextSize = 12, + }, + mapperContainer = new OsuHoverContainer + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = beatmap.Metadata.AuthorString, + TextSize = 12, + Font = @"Exo2.0-MediumItalic" + } + } + }, + } + }); + + RightFlowContainer.Add(new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + new OsuSpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Text = playCount.ToString(), + TextSize = 18, + Font = @"Exo2.0-SemiBoldItalic" + }, + new OsuSpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Text = @"times played ", + TextSize = 12, + Font = @"Exo2.0-RegularItalic" + }, + } + }); + + if (profileOverlay != null) + mapperContainer.Action = () => profileOverlay.ShowUser(beatmap.BeatmapSet.Metadata.Author); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs new file mode 100644 index 0000000000..e54f012faf --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests; +using osu.Game.Users; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer + { + public PaginatedMostPlayedBeatmapContainer(Bindable user) + :base(user, "Most Played Beatmaps", "No records. :(") + { + ItemsPerPage = 5; + + ItemsContainer.Direction = FillDirection.Vertical; + } + + protected override void ShowMore() + { + base.ShowMore(); + + var req = new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); + + req.Success += beatmaps => + { + ShowMoreButton.FadeTo(beatmaps.Count == ItemsPerPage ? 1 : 0); + ShowMoreLoading.Hide(); + + if (!beatmaps.Any() && VisiblePages == 1) + { + MissingText.Show(); + return; + } + + MissingText.Hide(); + + foreach (var beatmap in beatmaps) + { + ItemsContainer.Add(new DrawableMostPlayedRow(beatmap.GetBeatmapInfo(Rulesets), beatmap.PlayCount)); + } + }; + + Api.Queue(req); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index 78ed6bf846..ab99abdccd 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -1,6 +1,11 @@ // 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.Online.API.Requests; +using osu.Game.Overlays.Profile.Sections.Historical; +using osu.Game.Overlays.Profile.Sections.Ranks; + namespace osu.Game.Overlays.Profile.Sections { public class HistoricalSection : ProfileSection @@ -8,5 +13,14 @@ namespace osu.Game.Overlays.Profile.Sections public override string Title => "Historical"; public override string Identifier => "historical"; + + public HistoricalSection() + { + Children = new Drawable[] + { + new PaginatedMostPlayedBeatmapContainer(User), + new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", "No performance records. :("), + }; + } } } diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs new file mode 100644 index 0000000000..8a835634b8 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs @@ -0,0 +1,134 @@ +// 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 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.Sprites; +using osu.Game.Users; + +namespace osu.Game.Overlays.Profile.Sections.Kudosu +{ + public class KudosuInfo : Container + { + private readonly Bindable user = new Bindable(); + + public KudosuInfo(Bindable user) + { + this.user.BindTo(user); + + CountSection total; + CountSection avaliable; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Masking = true; + CornerRadius = 3; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 1f), + Radius = 3f, + Colour = Color4.Black.Opacity(0.2f), + }; + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.2f) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] + { + total = new CountSection( + "Total Kudosu Earned", + "Based on how much of a contribution the user has made to beatmap moderation. See this link for more information." + ), + avaliable = new CountSection( + "Kudosu Avaliable", + "Kudosu can be traded for kudosu stars, which will help your beatmap get more attention. This is the number of kudosu you haven't traded in yet." + ), + } + } + }; + + this.user.ValueChanged += newUser => + { + total.Count = newUser?.Kudosu.Total ?? 0; + avaliable.Count = newUser?.Kudosu.Available ?? 0; + }; + } + + protected override bool OnClick(InputState state) => true; + + private class CountSection : Container + { + private readonly OsuSpriteText valueText; + + public int Count + { + set { valueText.Text = value.ToString(); } + } + + public CountSection(string header, string description) + { + RelativeSizeAxes = Axes.X; + Width = 0.5f; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding { Horizontal = 10, Top = 10, Bottom = 20 }; + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = header + ":", + TextSize = 20, + Font = @"Exo2.0-RegularItalic", + }, + valueText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = "0", + TextSize = 40, + UseFullGlyphHeight = false, + Font = @"Exo2.0-RegularItalic" + } + } + }, + new TextFlowContainer(t => { t.TextSize = 19; }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = description + } + } + }; + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs index 3c36368fd7..907c26e5e8 100644 --- a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs +++ b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Overlays.Profile.Sections.Kudosu; + namespace osu.Game.Overlays.Profile.Sections { public class KudosuSection : ProfileSection @@ -8,5 +10,13 @@ namespace osu.Game.Overlays.Profile.Sections public override string Title => "Kudosu!"; public override string Identifier => "kudosu"; + + public KudosuSection() + { + Children = new[] + { + new KudosuInfo(User), + }; + } } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs new file mode 100644 index 0000000000..1d4045c379 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -0,0 +1,110 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Rulesets; +using osu.Game.Users; + +namespace osu.Game.Overlays.Profile.Sections +{ + public class PaginatedContainer : FillFlowContainer + { + protected readonly FillFlowContainer ItemsContainer; + protected readonly OsuHoverContainer ShowMoreButton; + protected readonly LoadingAnimation ShowMoreLoading; + protected readonly OsuSpriteText MissingText; + + protected int VisiblePages; + protected int ItemsPerPage; + + protected readonly Bindable User = new Bindable(); + + protected APIAccess Api; + protected RulesetStore Rulesets; + + public PaginatedContainer(Bindable user, string header, string missing) + { + User.BindTo(user); + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + + Children = new Drawable[] + { + new OsuSpriteText + { + TextSize = 15, + Text = header, + Font = "Exo2.0-RegularItalic", + Margin = new MarginPadding { Top = 10, Bottom = 10 }, + }, + ItemsContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Bottom = 10 } + }, + ShowMoreButton = new OsuHoverContainer + { + Alpha = 0, + Action = ShowMore, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Child = new OsuSpriteText + { + TextSize = 14, + Text = "show more", + } + }, + ShowMoreLoading = new LoadingAnimation + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(14), + }, + MissingText = new OsuSpriteText + { + TextSize = 14, + Text = missing, + Alpha = 0, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(APIAccess api, RulesetStore rulesets) + { + Api = api; + Rulesets = rulesets; + + User.ValueChanged += onUserChanged; + User.TriggerChange(); + } + + private void onUserChanged(User newUser) + { + VisiblePages = 0; + ItemsContainer.Clear(); + ShowMoreButton.Hide(); + + if (newUser != null) + ShowMore(); + } + + protected virtual void ShowMore() + { + ShowMoreLoading.Show(); + ShowMoreButton.Hide(); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawablePerformanceScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawablePerformanceScore.cs new file mode 100644 index 0000000000..c3296dae4f --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawablePerformanceScore.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Overlays.Profile.Sections.Ranks +{ + public class DrawablePerformanceScore : DrawableProfileScore + { + private readonly double? weight; + + public DrawablePerformanceScore(Score score, double? weight = null) + : base(score) + { + this.weight = weight; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + double pp = Score.PP ?? 0; + RightFlowContainer.Add(new OsuSpriteText + { + Text = $"{pp:0}pp", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + TextSize = 18, + Font = "Exo2.0-BoldItalic", + }); + + if (weight.HasValue) + { + RightFlowContainer.Add(new OsuSpriteText + { + Text = $"weighted: {pp * weight:0}pp ({weight:P0})", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = colour.GrayA, + TextSize = 11, + Font = "Exo2.0-RegularItalic", + }); + } + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs new file mode 100644 index 0000000000..06e4684d4c --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.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 OpenTK; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Select.Leaderboards; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Overlays.Profile.Sections.Ranks +{ + public abstract class DrawableProfileScore : DrawableProfileRow + { + private readonly FillFlowContainer metadata; + private readonly ScoreModsContainer modsContainer; + protected readonly Score Score; + + protected DrawableProfileScore(Score score) + { + Score = score; + + RelativeSizeAxes = Axes.X; + Height = 60; + Children = new Drawable[] + { + modsContainer = new ScoreModsContainer + { + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 60, + Margin = new MarginPadding { Right = 160 } + } + }; + } + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colour) + { + RightFlowContainer.Add(new OsuSpriteText + { + Text = $"accuracy: {Score.Accuracy:P2}", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Colour = colour.GrayA, + TextSize = 11, + Font = "Exo2.0-RegularItalic", + Depth = -1, + }); + + LeftFlowContainer.Add(new BeatmapMetadataContainer(Score.Beatmap)); + LeftFlowContainer.Add(new OsuSpriteText + { + Text = Score.Date.LocalDateTime.ToShortDateString(), + TextSize = 11, + Colour = OsuColour.Gray(0xAA), + }); + + foreach (Mod mod in Score.Mods) + modsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.5f) }); + } + + protected override Drawable CreateLeftVisual() => new DrawableRank(Score.Rank) + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableTotalScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableTotalScore.cs new file mode 100644 index 0000000000..56b2950f89 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableTotalScore.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.Graphics.Sprites; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Overlays.Profile.Sections.Ranks +{ + public class DrawableTotalScore : DrawableProfileScore + { + public DrawableTotalScore(Score score) + : base(score) + { + } + + [BackgroundDependencyLoader] + private void load() + { + RightFlowContainer.Add(new OsuSpriteText + { + Text = Score.TotalScore.ToString("#,###"), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + TextSize = 18, + Font = "Exo2.0-BoldItalic", + }); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs new file mode 100644 index 0000000000..472800860c --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.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 osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API.Requests; +using osu.Game.Users; +using System; +using System.Linq; + +namespace osu.Game.Overlays.Profile.Sections.Ranks +{ + public class PaginatedScoreContainer : PaginatedContainer + { + private readonly bool includeWeight; + private readonly ScoreType type; + + public PaginatedScoreContainer(ScoreType type, Bindable user, string header, string missing, bool includeWeight = false) + : base(user, header, missing) + { + this.type = type; + this.includeWeight = includeWeight; + + ItemsPerPage = 5; + + ItemsContainer.Direction = FillDirection.Vertical; + } + + protected override void ShowMore() + { + base.ShowMore(); + + var req = new GetUserScoresRequest(User.Value.Id, type, VisiblePages++ * ItemsPerPage); + + req.Success += scores => + { + foreach (var s in scores) + s.ApplyRuleset(Rulesets.GetRuleset(s.OnlineRulesetID)); + + ShowMoreButton.FadeTo(scores.Count == ItemsPerPage ? 1 : 0); + ShowMoreLoading.Hide(); + + if (!scores.Any() && VisiblePages == 1) + { + MissingText.Show(); + return; + } + + MissingText.Hide(); + + foreach (OnlineScore score in scores) + { + DrawableProfileScore drawableScore; + + switch (type) + { + default: + drawableScore = new DrawablePerformanceScore(score, includeWeight ? Math.Pow(0.95, ItemsContainer.Count) : (double?)null); + break; + case ScoreType.Recent: + drawableScore = new DrawableTotalScore(score); + break; + } + + ItemsContainer.Add(drawableScore); + } + }; + + Api.Queue(req); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/ScoreModsContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/ScoreModsContainer.cs new file mode 100644 index 0000000000..6f050750e4 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Ranks/ScoreModsContainer.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 OpenTK; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.UI; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Overlays.Profile.Sections.Ranks +{ + public class ScoreModsContainer : FlowContainer + { + protected override IEnumerable ComputeLayoutPositions() + { + int count = FlowingChildren.Count(); + for (int i = 0; i < count; i++) + yield return new Vector2(DrawWidth * i * (count == 1 ? 0 : 1f / (count - 1)), 0); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 5ea135fcac..7691100d7a 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -1,6 +1,9 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Overlays.Profile.Sections.Ranks; +using osu.Game.Online.API.Requests; + namespace osu.Game.Overlays.Profile.Sections { public class RanksSection : ProfileSection @@ -8,5 +11,14 @@ namespace osu.Game.Overlays.Profile.Sections public override string Title => "Ranks"; public override string Identifier => "top_ranks"; + + public RanksSection() + { + Children = new[] + { + new PaginatedScoreContainer(ScoreType.Best, User, "Best Performance", "No performance records. :(", true), + new PaginatedScoreContainer(ScoreType.Firsts, User, "First Place Ranks", "No awesome performance records yet. :("), + }; + } } } diff --git a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs new file mode 100644 index 0000000000..7c324658a3 --- /dev/null +++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.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.Allocation; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.Settings +{ + /// + /// A with pink colours to mark dangerous/destructive actions. + /// + public class DangerousSettingsButton : SettingsButton + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Pink; + + Triangles.ColourDark = colours.PinkDark; + Triangles.ColourLight = colours.PinkLight; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index bc09a2145a..4ae127c816 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -20,11 +20,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsSlider { LabelText = "Audio Offset", - Bindable = config.GetBindable(OsuSetting.AudioOffset) + Bindable = config.GetBindable(OsuSetting.AudioOffset), + KeyboardStep = 100f }, - new OsuButton + new SettingsButton { - RelativeSizeAxes = Axes.X, Text = "Offset wizard" } }; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs index ea442cdfc2..d197f8c466 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs @@ -16,9 +16,9 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { Children = new Drawable[] { - new SettingsSlider { LabelText = "Master", Bindable = audio.Volume }, - new SettingsSlider { LabelText = "Effect", Bindable = audio.VolumeSample }, - new SettingsSlider { LabelText = "Music", Bindable = audio.VolumeTrack }, + new SettingsSlider { LabelText = "Master", Bindable = audio.Volume, KeyboardStep = 0.1f }, + new SettingsSlider { LabelText = "Effect", Bindable = audio.VolumeSample, KeyboardStep = 0.1f }, + new SettingsSlider { LabelText = "Music", Bindable = audio.VolumeTrack, KeyboardStep = 0.1f }, }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs index 495a2543d1..23e7433732 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs @@ -6,7 +6,6 @@ using System.Runtime; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections.Debug { @@ -24,9 +23,8 @@ namespace osu.Game.Overlays.Settings.Sections.Debug LabelText = "Active mode", Bindable = config.GetBindable(DebugSetting.ActiveGCMode) }, - new OsuButton + new SettingsButton { - RelativeSizeAxes = Axes.X, Text = "Force garbage collection", Action = GC.Collect }, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index a8ec04514a..8ec6af5cd0 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -19,7 +19,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsSlider { LabelText = "Background dim", - Bindable = config.GetBindable(OsuSetting.DimLevel) + Bindable = config.GetBindable(OsuSetting.DimLevel), + KeyboardStep = 0.1f }, new SettingsCheckbox { diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs index 08dba011df..03cd2118b9 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs @@ -17,21 +17,28 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { Children = new Drawable[] { + new SettingsCheckbox + { + LabelText = "Show converted beatmaps", + Bindable = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + }, new SettingsSlider { LabelText = "Display beatmaps from", - Bindable = config.GetBindable(OsuSetting.DisplayStarsMinimum) + Bindable = config.GetBindable(OsuSetting.DisplayStarsMinimum), + KeyboardStep = 1f }, new SettingsSlider { LabelText = "up to", - Bindable = config.GetBindable(OsuSetting.DisplayStarsMaximum) + Bindable = config.GetBindable(OsuSetting.DisplayStarsMaximum), + KeyboardStep = 1f }, - new SettingsEnumDropdown + new SettingsEnumDropdown { - LabelText = "Random beatmap selection", - Bindable = config.GetBindable(OsuSetting.SelectionRandomType), - }, + LabelText = "Random selection algorithm", + Bindable = config.GetBindable(OsuSetting.RandomSelectAlgorithm), + } }; } diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index 326cb582e2..035a3c7a13 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - foreach(Ruleset ruleset in rulesets.AllRulesets.Select(info => info.CreateInstance())) + foreach(Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance())) { SettingsSubsection section = ruleset.CreateSettings(); if (section != null) diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 8b7d7b0d69..56b9a55398 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -230,15 +230,13 @@ namespace osu.Game.Overlays.Settings.Sections.General LabelText = "Stay logged in", Bindable = config.GetBindable(OsuSetting.SavePassword), }, - new OsuButton + new SettingsButton { - RelativeSizeAxes = Axes.X, Text = "Sign in", Action = performLogin }, - new OsuButton + new SettingsButton { - RelativeSizeAxes = Axes.X, Text = "Register new account", //Action = registerLink } diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 833a5ff966..3bca0af3af 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections.General { @@ -23,9 +22,8 @@ namespace osu.Game.Overlays.Settings.Sections.General LabelText = "Release stream", Bindable = config.GetBindable(OsuSetting.ReleaseStream), }, - new OsuButton + new SettingsButton { - RelativeSizeAxes = Axes.X, Text = "Open osu! folder", Action = storage.OpenInNativeExplorer, } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index c4ce742153..3d09d6b901 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -49,12 +49,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsSlider { LabelText = "Horizontal position", - Bindable = config.GetBindable(FrameworkSetting.LetterboxPositionX) + Bindable = config.GetBindable(FrameworkSetting.LetterboxPositionX), + KeyboardStep = 0.1f }, new SettingsSlider { LabelText = "Vertical position", - Bindable = config.GetBindable(FrameworkSetting.LetterboxPositionY) + Bindable = config.GetBindable(FrameworkSetting.LetterboxPositionY), + KeyboardStep = 0.1f }, } }, diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs index b68fd4bc04..00a1c093fd 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections.Input { @@ -14,9 +13,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { Children = new Drawable[] { - new OsuButton + new SettingsButton { - RelativeSizeAxes = Axes.X, Text = "Key Configuration", Action = keyConfig.ToggleVisibility }, diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 5ebac37cc8..392bc6f1bd 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -97,6 +97,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input doubleValue.ValueChanged += newValue => base.Bindable.Value = newValue; } } + + public SensitivitySetting() + { + KeyboardStep = 0.01f; + } } private class SensitivitySlider : OsuSliderBar @@ -105,8 +110,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input public SensitivitySlider() { - KeyboardStep = 0.01f; - Current.ValueChanged += newValue => { if (!isDragging && Sensitivity != null) @@ -133,4 +136,4 @@ namespace osu.Game.Overlays.Settings.Sections.Input public override string TooltipText => Current.Disabled ? "Enable raw input to adjust sensitivity" : Current.Value.ToString(@"0.##x"); } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/DeleteAllBeatmapsDialog.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/DeleteAllBeatmapsDialog.cs new file mode 100644 index 0000000000..8ae520651a --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/DeleteAllBeatmapsDialog.cs @@ -0,0 +1,32 @@ +// 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.Graphics; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Overlays.Settings.Sections.Maintenance +{ + public class DeleteAllBeatmapsDialog : PopupDialog + { + public DeleteAllBeatmapsDialog(Action deleteAction) + { + BodyText = "Everything?"; + + Icon = FontAwesome.fa_trash_o; + HeaderText = @"Confirm deletion of"; + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Yes. Go for it.", + Action = deleteAction + }, + new PopupDialogCancelButton + { + Text = @"No! Abort mission!", + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 233ca7be60..e288445c6d 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.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 System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -11,51 +12,62 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { public class GeneralSettings : SettingsSubsection { - private OsuButton importButton; - private OsuButton deleteButton; - private OsuButton restoreButton; + private TriangleButton importButton; + private TriangleButton deleteButton; + private TriangleButton restoreButton; + private TriangleButton undeleteButton; protected override string Header => "General"; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps) + private void load(BeatmapManager beatmaps, DialogOverlay dialogOverlay) { Children = new Drawable[] { - importButton = new OsuButton + importButton = new SettingsButton { - RelativeSizeAxes = Axes.X, Text = "Import beatmaps from stable", Action = () => { importButton.Enabled.Value = false; - Task.Run(() => beatmaps.ImportFromStable()).ContinueWith(t => Schedule(() => importButton.Enabled.Value = true)); + Task.Factory.StartNew(beatmaps.ImportFromStable) + .ContinueWith(t => Schedule(() => importButton.Enabled.Value = true), TaskContinuationOptions.LongRunning); } }, - deleteButton = new OsuButton + deleteButton = new DangerousSettingsButton { - RelativeSizeAxes = Axes.X, Text = "Delete ALL beatmaps", Action = () => { - deleteButton.Enabled.Value = false; - Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); + dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => + { + deleteButton.Enabled.Value = false; + Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); + })); } }, - restoreButton = new OsuButton + restoreButton = new SettingsButton { - RelativeSizeAxes = Axes.X, Text = "Restore all hidden difficulties", Action = () => { restoreButton.Enabled.Value = false; Task.Run(() => { - foreach (var b in beatmaps.QueryBeatmaps(b => b.Hidden)) + foreach (var b in beatmaps.QueryBeatmaps(b => b.Hidden).ToList()) beatmaps.Restore(b); }).ContinueWith(t => Schedule(() => restoreButton.Enabled.Value = true)); } }, + undeleteButton = new SettingsButton + { + Text = "Restore all recently deleted beatmaps", + Action = () => + { + undeleteButton.Enabled.Value = false; + Task.Run(() => beatmaps.UndeleteAll()).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); + } + }, }; } } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 4b4426aca8..b4475aebb1 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -24,12 +24,14 @@ namespace osu.Game.Overlays.Settings.Sections new SettingsSlider { LabelText = "Menu cursor size", - Bindable = config.GetBindable(OsuSetting.MenuCursorSize) + Bindable = config.GetBindable(OsuSetting.MenuCursorSize), + KeyboardStep = 0.1f }, new SettingsSlider { LabelText = "Gameplay cursor size", - Bindable = config.GetBindable(OsuSetting.GameplayCursorSize) + Bindable = config.GetBindable(OsuSetting.GameplayCursorSize), + KeyboardStep = 0.1f }, new SettingsCheckbox { diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs new file mode 100644 index 0000000000..19493f6c70 --- /dev/null +++ b/osu.Game/Overlays/Settings/SettingsButton.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.Framework.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Settings +{ + public class SettingsButton : TriangleButton + { + public SettingsButton() + { + RelativeSizeAxes = Axes.X; + Padding = new MarginPadding { Left = SettingsOverlay.CONTENT_MARGINS, Right = SettingsOverlay.CONTENT_MARGINS }; + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index cb1c861ee7..bf417a2fac 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings var modes = new List(); - foreach (var ruleset in rulesets.AllRulesets) + foreach (var ruleset in rulesets.AvailableRulesets) { var icon = new ConstrainedIconContainer { diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index f2044f178b..da50c2b444 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -2,17 +2,24 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using osu.Framework.Allocation; using OpenTK.Graphics; using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +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; namespace osu.Game.Overlays.Settings { - public abstract class SettingsItem : FillFlowContainer, IFilterable + public abstract class SettingsItem : Container, IFilterable { protected abstract Drawable CreateControl(); @@ -20,8 +27,28 @@ namespace osu.Game.Overlays.Settings private IHasCurrentValue controlWithCurrent => Control as IHasCurrentValue; + protected override Container Content => FlowContent; + + protected readonly FillFlowContainer FlowContent; + private SpriteText text; + private readonly RestoreDefaultValueButton restoreDefaultValueButton = new RestoreDefaultValueButton(); + + public bool ShowsDefaultIndicator = true; + + private Color4? restoreDefaultValueColour; + + public Color4 RestoreDefaultValueColour + { + get { return restoreDefaultValueColour ?? Color4.White; } + set + { + restoreDefaultValueColour = value; + restoreDefaultValueButton?.SetButtonColour(RestoreDefaultValueColour); + } + } + public virtual string LabelText { get { return text?.Text ?? string.Empty; } @@ -51,6 +78,11 @@ namespace osu.Game.Overlays.Settings { bindable = value; controlWithCurrent?.Current.BindTo(bindable); + if (ShowsDefaultIndicator) + { + restoreDefaultValueButton.Bindable = bindable.GetBoundCopy(); + restoreDefaultValueButton.Bindable.TriggerChange(); + } } } @@ -69,13 +101,101 @@ namespace osu.Game.Overlays.Settings { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Padding = new MarginPadding { Right = 5 }; + Padding = new MarginPadding { Right = SettingsOverlay.CONTENT_MARGINS }; + + FlowContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = SettingsOverlay.CONTENT_MARGINS, Right = 5 }, + }; if ((Control = CreateControl()) != null) { if (controlWithCurrent != null) controlWithCurrent.Current.DisabledChanged += disabled => { Colour = disabled ? Color4.Gray : Color4.White; }; - Add(Control); + FlowContent.Add(Control); + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AddInternal(FlowContent); + + if (restoreDefaultValueButton != null) + { + if (!restoreDefaultValueColour.HasValue) + restoreDefaultValueColour = colours.Yellow; + restoreDefaultValueButton.SetButtonColour(RestoreDefaultValueColour); + AddInternal(restoreDefaultValueButton); + } + } + + private class RestoreDefaultValueButton : Box, IHasTooltip + { + private Bindable bindable; + public Bindable Bindable + { + get { return bindable; } + set + { + bindable = value; + bindable.ValueChanged += newValue => UpdateState(); + bindable.DisabledChanged += disabled => UpdateState(); + } + } + + private Color4 buttonColour; + + private bool hovering; + + public RestoreDefaultValueButton() + { + RelativeSizeAxes = Axes.Y; + Width = SettingsOverlay.CONTENT_MARGINS; + Alpha = 0f; + } + + public string TooltipText => "Revert to default"; + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => true; + + protected override bool OnClick(InputState state) + { + if (bindable != null && !bindable.Disabled) + bindable.SetDefault(); + return true; + } + + protected override bool OnHover(InputState state) + { + hovering = true; + UpdateState(); + return true; + } + + protected override void OnHoverLost(InputState state) + { + hovering = false; + UpdateState(); + } + + public void SetButtonColour(Color4 buttonColour) + { + this.buttonColour = buttonColour; + UpdateState(); + } + + public void UpdateState() + { + if (bindable == null) + return; + var colour = bindable.Disabled ? Color4.Gray : buttonColour; + this.FadeTo(bindable.IsDefault ? 0f : hovering && !bindable.Disabled ? 1f : 0.5f, 200, Easing.OutQuint); + this.FadeColour(ColourInfo.GradientHorizontal(colour.Opacity(0.8f), colour.Opacity(0)), 200, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Settings/SettingsLabel.cs b/osu.Game/Overlays/Settings/SettingsLabel.cs index 7d1364ef41..6a1d3ae72c 100644 --- a/osu.Game/Overlays/Settings/SettingsLabel.cs +++ b/osu.Game/Overlays/Settings/SettingsLabel.cs @@ -7,7 +7,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Settings { - internal class SettingsLabel : SettingsItem + public class SettingsLabel : SettingsItem { protected override Drawable CreateControl() => null; diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index eb6e398477..a107878fb8 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -69,8 +69,6 @@ namespace osu.Game.Overlays.Settings Padding = new MarginPadding { Top = 20 + border_size, - Left = SettingsOverlay.CONTENT_MARGINS, - Right = SettingsOverlay.CONTENT_MARGINS, Bottom = 10, }, RelativeSizeAxes = Axes.X, @@ -81,7 +79,8 @@ namespace osu.Game.Overlays.Settings { TextSize = header_size, Text = Header, - Colour = colours.Yellow + Colour = colours.Yellow, + Margin = new MarginPadding { Left = SettingsOverlay.CONTENT_MARGINS, Right = SettingsOverlay.CONTENT_MARGINS } }, FlowContent } diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs index 2881d02302..49d73f77ec 100644 --- a/osu.Game/Overlays/Settings/SettingsSlider.cs +++ b/osu.Game/Overlays/Settings/SettingsSlider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; @@ -22,5 +23,15 @@ namespace osu.Game.Overlays.Settings Margin = new MarginPadding { Top = 5, Bottom = 5 }, RelativeSizeAxes = Axes.X }; + + public float KeyboardStep; + + [BackgroundDependencyLoader] + private void load() + { + var slider = Control as U; + if (slider != null) + slider.KeyboardStep = KeyboardStep; + } } } diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index 4164ceee21..79a644b2cb 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Settings new OsuSpriteText { Text = Header.ToUpper(), - Margin = new MarginPadding { Bottom = 10 }, + Margin = new MarginPadding { Bottom = 10, Left = SettingsOverlay.CONTENT_MARGINS, Right = SettingsOverlay.CONTENT_MARGINS }, Font = @"Exo2.0-Black", }, FlowContent diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index 55167188a3..4e51ae3a2e 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -18,8 +18,8 @@ namespace osu.Game.Overlays.Settings public class Sidebar : Container, IStateful { private readonly FillFlowContainer content; - internal const float DEFAULT_WIDTH = ToolbarButton.WIDTH; - internal const int EXPANDED_WIDTH = 200; + public const float DEFAULT_WIDTH = ToolbarButton.WIDTH; + public const int EXPANDED_WIDTH = 200; public event Action StateChanged; @@ -137,4 +137,4 @@ namespace osu.Game.Overlays.Settings Contracted, Expanded, } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index b39c8ab7cf..4b8366f0fc 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -12,14 +12,14 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { - public class SidebarButton : Container + public class SidebarButton : OsuButton { private readonly SpriteIcon drawableIcon; private readonly SpriteText headerText; - private readonly Box backgroundBox; private readonly Box selectionIndicator; private readonly Container text; public Action Action; @@ -61,17 +61,14 @@ namespace osu.Game.Overlays.Settings public SidebarButton() { + BackgroundColour = OsuColour.Gray(60); + Background.Alpha = 0; + Height = Sidebar.DEFAULT_WIDTH; RelativeSizeAxes = Axes.X; - Children = new Drawable[] + + AddRange(new Drawable[] { - backgroundBox = new Box - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Colour = OsuColour.Gray(60), - Alpha = 0, - }, text = new Container { Width = Sidebar.DEFAULT_WIDTH, @@ -101,7 +98,7 @@ namespace osu.Game.Overlays.Settings Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, } - }; + }); } [BackgroundDependencyLoader] @@ -113,20 +110,19 @@ namespace osu.Game.Overlays.Settings protected override bool OnClick(InputState state) { Action?.Invoke(section); - backgroundBox.FlashColour(Color4.White, 400); - return true; + return base.OnClick(state); } protected override bool OnHover(InputState state) { - backgroundBox.FadeTo(0.4f, 200); + Background.FadeTo(0.4f, 200); return base.OnHover(state); } protected override void OnHoverLost(InputState state) { - backgroundBox.FadeTo(0, 200); + Background.FadeTo(0, 200); base.OnHoverLost(state); } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index ddef5d1001..798fa00032 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays { public abstract class SettingsOverlay : OsuFocusedOverlayContainer { - internal const float CONTENT_MARGINS = 10; + public const float CONTENT_MARGINS = 10; public const float TRANSITION_LENGTH = 600; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index cb17216679..c039f9d311 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -13,6 +13,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Toolbar { @@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Toolbar private readonly SpriteText tooltip2; protected FillFlowContainer Flow; - public ToolbarButton() + public ToolbarButton() : base(HoverSampleSet.Loud) { Width = WIDTH; RelativeSizeAxes = Axes.Y; @@ -195,4 +196,4 @@ namespace osu.Game.Overlays.Toolbar }; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs index 2e2786851c..ed206e7e1d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs @@ -6,7 +6,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarChatButton : ToolbarOverlayToggleButton + public class ToolbarChatButton : ToolbarOverlayToggleButton { public ToolbarChatButton() { @@ -19,4 +19,4 @@ namespace osu.Game.Overlays.Toolbar StateContainer = chat; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs b/osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs index dacb6d67b8..7d25440e2c 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs @@ -6,7 +6,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarDirectButton : ToolbarOverlayToggleButton + public class ToolbarDirectButton : ToolbarOverlayToggleButton { public ToolbarDirectButton() { @@ -19,4 +19,4 @@ namespace osu.Game.Overlays.Toolbar StateContainer = direct; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index 431cc73887..9f020cada5 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -5,7 +5,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarHomeButton : ToolbarButton + public class ToolbarHomeButton : ToolbarButton { public ToolbarHomeButton() { @@ -14,4 +14,4 @@ namespace osu.Game.Overlays.Toolbar TooltipSub = "Return to the main menu"; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs index 60c1261190..319dd63bc9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarModeSelector : Container + public class ToolbarModeSelector : Container { private const float padding = 10; @@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load(RulesetStore rulesets, OsuGame game) { - foreach (var r in rulesets.AllRulesets) + foreach (var r in rulesets.AvailableRulesets) { modeButtons.Add(new ToolbarModeButton { diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index 82599b9a0d..d150aacdf9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -6,7 +6,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarMusicButton : ToolbarOverlayToggleButton + public class ToolbarMusicButton : ToolbarOverlayToggleButton { public ToolbarMusicButton() { @@ -19,4 +19,4 @@ namespace osu.Game.Overlays.Toolbar StateContainer = music; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs index dcadc4bf56..e11a22d675 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs @@ -7,7 +7,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarNotificationButton : ToolbarOverlayToggleButton + public class ToolbarNotificationButton : ToolbarOverlayToggleButton { protected override Anchor TooltipAnchor => Anchor.TopRight; @@ -24,4 +24,4 @@ namespace osu.Game.Overlays.Toolbar StateContainer = notificationOverlay; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 28ecf6ad03..69fdd27d5d 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarOverlayToggleButton : ToolbarButton + public class ToolbarOverlayToggleButton : ToolbarButton { private readonly Box stateBackground; diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs index 2eb8c15dcf..cf4f664e81 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs @@ -6,7 +6,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarSettingsButton : ToolbarOverlayToggleButton + public class ToolbarSettingsButton : ToolbarOverlayToggleButton { public ToolbarSettingsButton() { @@ -21,4 +21,4 @@ namespace osu.Game.Overlays.Toolbar StateContainer = settings; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index ed36fd8f9e..234d6f0f9a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -6,7 +6,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarSocialButton : ToolbarOverlayToggleButton + public class ToolbarSocialButton : ToolbarOverlayToggleButton { public ToolbarSocialButton() { @@ -19,4 +19,4 @@ namespace osu.Game.Overlays.Toolbar StateContainer = chat; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserArea.cs b/osu.Game/Overlays/Toolbar/ToolbarUserArea.cs index c1fd234628..4562464dfe 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserArea.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserArea.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.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -8,14 +9,15 @@ using OpenTK; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarUserArea : Container + public class ToolbarUserArea : Container { public LoginOverlay LoginOverlay; - private readonly ToolbarUserButton button; + private ToolbarUserButton button; public override RectangleF BoundingBox => button.BoundingBox; - public ToolbarUserArea() + [BackgroundDependencyLoader] + private void load() { RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; @@ -36,4 +38,4 @@ namespace osu.Game.Overlays.Toolbar }; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index deacfbb9ec..3714094924 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -12,7 +12,7 @@ using OpenTK.Graphics; namespace osu.Game.Overlays.Toolbar { - internal class ToolbarUserButton : ToolbarButton, IOnlineComponent + public class ToolbarUserButton : ToolbarButton, IOnlineComponent { private readonly UpdateableAvatar avatar; diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index f03ef3f1ed..7374a9aa44 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -34,6 +34,7 @@ namespace osu.Game.Overlays public const float CONTENT_X_MARGIN = 50; + // receive input outside our bounds so we can trigger a close event on ourselves. public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; protected override bool OnClick(InputState state) @@ -90,10 +91,10 @@ namespace osu.Game.Overlays sections = new ProfileSection[] { - new AboutSection(), - new RecentSection(), + //new AboutSection(), + //new RecentSection(), new RanksSection(), - new MedalsSection(), + //new MedalsSection(), new HistoricalSection(), new BeatmapsSection(), new KudosuSection() @@ -163,6 +164,7 @@ namespace osu.Game.Overlays } Show(); + sectionsContainer.ScrollToTop(); } private void userLoadComplete(User user) @@ -174,6 +176,8 @@ namespace osu.Game.Overlays var sec = sections.FirstOrDefault(s => s.Identifier == id); if (sec != null) { + sec.User.Value = user; + sectionsContainer.Add(sec); tabs.AddItem(sec); } diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 4f9783a762..3f9703a523 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.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; using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Containers; @@ -29,6 +28,8 @@ namespace osu.Game.Overlays private readonly Container contentContainer; + protected override bool BlockPassThroughKeyboard => true; + protected override Container Content => contentContainer; protected Color4 FirstWaveColour @@ -165,12 +166,12 @@ namespace osu.Game.Overlays wavesContainer.Height = Math.Max(0, DrawHeight - (contentContainer.DrawHeight - contentContainer.Y)); } - private class Wave : Container, IStateful + private class Wave : VisibilityContainer { - public event Action StateChanged; - public float FinalPosition; + protected override bool StartHidden => true; + public Wave() { RelativeSizeAxes = Axes.X; @@ -183,13 +184,7 @@ namespace osu.Game.Overlays Radius = 20f, }; - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - }, - }; + Child = new Box { RelativeSizeAxes = Axes.Both }; } protected override void Update() @@ -201,28 +196,8 @@ namespace osu.Game.Overlays Height = Parent.Parent.DrawSize.Y * 1.5f; } - private Visibility state; - - public Visibility State - { - get { return state; } - set - { - state = value; - - switch (value) - { - case Visibility.Hidden: - this.MoveToY(Parent.Parent.DrawSize.Y, DISAPPEAR_DURATION, easing_hide); - break; - case Visibility.Visible: - this.MoveToY(FinalPosition, APPEAR_DURATION, easing_show); - break; - } - - StateChanged?.Invoke(State); - } - } + protected override void PopIn() => this.MoveToY(FinalPosition, APPEAR_DURATION, easing_show); + protected override void PopOut() => this.MoveToY(Parent.Parent.DrawSize.Y, DISAPPEAR_DURATION, easing_hide); } } } diff --git a/osu.Game/Properties/AssemblyInfo.cs b/osu.Game/Properties/AssemblyInfo.cs index fe7ad20124..e28f8a3873 100644 --- a/osu.Game/Properties/AssemblyInfo.cs +++ b/osu.Game/Properties/AssemblyInfo.cs @@ -7,11 +7,11 @@ using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("osu!lazer")] +[assembly: AssemblyTitle("osu.Game")] [assembly: AssemblyDescription("click the circles. to the beat.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("ppy Pty Ltd")] -[assembly: AssemblyProduct("osu!lazer")] +[assembly: AssemblyProduct("osu.Game")] [assembly: AssemblyCopyright("ppy Pty Ltd 2007-2017")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -24,5 +24,5 @@ using System.Runtime.InteropServices; // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("55e28cb2-7b6c-4595-8dcc-9871d8aad7e9")] -[assembly: AssemblyVersion("0.0.0")] -[assembly: AssemblyFileVersion("0.0.0")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/osu.Game/Resources/Resource.cs b/osu.Game/Resources/Resource.cs deleted file mode 100644 index 6c66b6818b..0000000000 --- a/osu.Game/Resources/Resource.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 System; -using System.IO; -using System.Reflection; - -namespace osu.Game.Tests.Resources -{ - public static class Resource - { - public static Stream OpenResource(string name) - { - var localPath = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path)); - - return Assembly.GetExecutingAssembly().GetManifestResourceStream($@"osu.Game.Tests.Resources.{name}") ?? - Assembly.LoadFrom(Path.Combine(localPath, @"osu.Game.Resources.dll")).GetManifestResourceStream($@"osu.Game.Resources.{name}"); - } - } -} \ No newline at end of file diff --git a/osu.Game/Resources/Soleily - Renatus (Gamu) [Insane].osu b/osu.Game/Resources/Soleily - Renatus (Gamu) [Insane].osu deleted file mode 100644 index 3e44dc0af8..0000000000 --- a/osu.Game/Resources/Soleily - Renatus (Gamu) [Insane].osu +++ /dev/null @@ -1,1002 +0,0 @@ -osu file format v14 - -[General] -AudioFilename: 03. Renatus - Soleily 192kbps.mp3 -AudioLeadIn: 0 -PreviewTime: 164471 -Countdown: 0 -SampleSet: Soft -StackLeniency: 0.7 -Mode: 0 -LetterboxInBreaks: 0 -WidescreenStoryboard: 0 - -[Editor] -Bookmarks: 11505,22054,32604,43153,53703,64252,74802,85351,95901,106450,116999,119637,130186,140735,151285,161834,164471,175020,185570,196119,206669,209306 -DistanceSpacing: 1.8 -BeatDivisor: 4 -GridSize: 4 -TimelineZoom: 2 - -[Metadata] -Title:Renatus -TitleUnicode:Renatus -Artist:Soleily -ArtistUnicode:Soleily -Creator:Gamu -Version:Insane -Source: -Tags:MBC7 Unisphere 地球ヤバイEP Chikyu Yabai -BeatmapID:557821 -BeatmapSetID:241526 - -[Difficulty] -HPDrainRate:6.5 -CircleSize:4 -OverallDifficulty:8 -ApproachRate:9 -SliderMultiplier:1.8 -SliderTickRate:2 - -[Events] -//Background and Video events -0,0,"machinetop_background.jpg",0,0 -//Break Periods -2,122474,140135 -//Storyboard Layer 0 (Background) -//Storyboard Layer 1 (Fail) -//Storyboard Layer 2 (Pass) -//Storyboard Layer 3 (Foreground) -//Storyboard Sound Samples - -[TimingPoints] -956,329.67032967033,4,2,0,60,1,0 -20736,-100,4,2,0,65,0,0 -22054,-100,4,2,0,70,0,0 -43153,-100,4,2,0,60,0,0 -48428,-100,4,2,0,50,0,0 -52879,-100,4,2,0,50,0,0 -53373,-100,4,2,0,60,0,0 -53703,-100,4,2,0,70,0,1 -74719,-100,4,2,0,70,0,0 -74802,-100,4,2,0,70,0,1 -95901,-100,4,2,0,70,0,0 -116999,-133.333333333333,4,2,0,50,0,0 -117164,-133.333333333333,4,2,0,30,0,0 -117329,-79.9999999999999,4,2,0,50,0,0 -117659,-100,4,2,0,50,0,0 -118977,-100,4,2,0,60,0,0 -119307,-100,4,2,0,70,0,0 -119637,659.340659340659,4,2,0,80,1,0 -119966,-100,4,2,0,70,0,0 -120296,-100,4,2,0,60,0,0 -120626,-100,4,2,0,50,0,0 -120955,-100,4,2,0,40,0,0 -121285,-100,4,2,0,30,0,0 -121615,-100,4,2,0,20,0,0 -121944,-100,4,2,0,10,0,0 -122274,-100,4,2,0,5,0,0 -140735,-100,4,2,0,50,0,0 -151285,-80,4,2,0,60,0,0 -161834,329.67032967033,4,2,0,65,1,0 -164141,-100,4,2,0,70,0,0 -164471,-100,4,2,0,70,0,1 -185487,-100,4,2,0,70,0,0 -185570,-100,4,2,0,70,0,1 -206669,659.340659340659,4,2,0,80,1,0 -206998,-100,4,2,0,70,0,0 -207328,-100,4,2,0,60,0,0 -207658,-100,4,2,0,50,0,0 -207987,-100,4,2,0,40,0,0 -208317,-100,4,2,0,30,0,0 -208647,-100,4,2,0,20,0,0 -208976,-100,4,2,0,10,0,0 -209306,-100,4,2,0,5,0,0 - - -[Colours] -Combo1 : 142,199,255 -Combo2 : 255,128,128 -Combo3 : 128,255,255 -Combo4 : 128,255,128 -Combo5 : 255,187,255 -Combo6 : 255,177,140 - -[HitObjects] -192,168,956,6,0,P|184:128|200:80,1,90,4|0,1:2|0:0,0:0:0:0: -304,56,1285,1,8,0:0:0:0: -244,236,1450,2,0,P|204:252|156:244,1,90,2|0,0:0|0:0,0:0:0:0: -276,156,1780,2,0,P|310:181|329:226,1,90,2|8,1:2|0:0,0:0:0:0: -300,328,2109,1,2,0:0:0:0: -192,332,2274,6,0,L|144:340,2,45,0|0|0,1:0|0:0|0:0,0:0:0:0: -388,300,2604,1,8,0:0:0:0: -244,236,2769,1,0,1:0:0:0: -232,208,2851,1,0,0:0:0:0: -224,176,2934,1,0,0:0:0:0: -228,144,3016,1,0,0:0:0:0: -244,116,3098,1,0,1:0:0:0: -332,52,3263,2,0,P|376:48|424:56,1,90,8|0,0:0|0:0,0:0:0:0: -488,228,3593,5,0,1:0:0:0: -460,240,3675,1,0,0:0:0:0: -428,236,3758,1,0,0:0:0:0: -292,160,3923,2,0,P|288:204|300:252,1,90,8|0,0:0|0:0,0:0:0:0: -316,276,4170,1,0,0:0:0:0: -344,292,4252,2,0,L|388:300,1,45,0|0,0:0|0:0,0:0:0:0: -288,356,4417,2,0,L|244:364,1,45,0|0,1:0|0:0,0:0:0:0: -168,328,4582,2,0,P|124:324|72:332,1,90,8|0,0:0|0:0,0:0:0:0: -24,188,4912,5,0,1:0:0:0: -56,192,4994,1,0,0:0:0:0: -88,196,5076,1,0,0:0:0:0: -148,108,5241,1,8,0:0:0:0: -188,240,5406,1,0,1:0:0:0: -188,240,5488,1,0,0:0:0:0: -188,240,5571,2,0,L|168:328,1,90,0|0,0:0|1:0,0:0:0:0: -260,216,5901,2,0,P|236:180|188:164,1,90,8|0,0:0|0:0,0:0:0:0: -248,296,6230,6,0,L|348:292,1,90,0|0,1:0|0:0,0:0:0:0: -504,232,6560,1,8,0:0:0:0: -400,204,6725,1,0,0:0:0:0: -392,176,6807,1,0,0:0:0:0: -384,144,6890,1,0,0:0:0:0: -376,116,6972,1,0,0:0:0:0: -368,88,7054,1,0,1:0:0:0: -188,48,7219,2,0,L|208:140,1,90,8|0,0:0|0:0,0:0:0:0: -248,296,7549,5,0,1:0:0:0: -207,135,7714,1,0,0:0:0:0: -156,232,7879,1,8,0:0:0:0: -316,191,8043,1,0,1:0:0:0: -316,191,8126,1,0,0:0:0:0: -316,191,8208,2,0,L|372:200,1,45,0|0,0:0|0:0,0:0:0:0: -492,200,8373,2,0,L|447:207,1,45,0|0,1:0|0:0,0:0:0:0: -408,136,8538,2,0,P|396:92|400:48,1,90,8|0,0:0|0:0,0:0:0:0: -260,32,8868,5,0,1:0:0:0: -252,64,8950,1,0,0:0:0:0: -236,92,9032,2,0,P|204:116|148:128,1,90,0|8,0:0|0:0,0:0:0:0: -28,188,9362,1,0,0:0:0:0: -60,196,9445,1,0,0:0:0:0: -88,212,9527,2,0,P|112:244|124:300,1,90,0|0,0:0|1:0,0:0:0:0: -112,128,9857,2,0,P|152:156|184:196,1,90,8|0,0:0|0:0,0:0:0:0: -216,288,10186,5,0,1:0:0:0: -216,288,10269,1,0,0:0:0:0: -216,288,10351,1,0,0:0:0:0: -268,192,10516,1,8,0:0:0:0: -356,128,10681,1,0,1:0:0:0: -388,120,10763,1,0,0:0:0:0: -420,128,10846,2,0,P|440:168|436:220,1,90,0|0,0:0|1:0,0:0:0:0: -332,328,11175,2,0,L|280:332,1,45,8|8,0:0|0:0,0:0:0:0: -216,288,11340,2,0,L|164:292,1,45,0|0,1:0|0:0,1:0:0:0: -100,248,11505,5,4,1:2:0:0: -148,116,11670,1,2,0:0:0:0: -268,192,11835,1,10,0:0:0:0: -136,328,11999,2,0,L|44:336,1,90,2|0,0:0|0:0,0:0:0:0: -216,288,12329,1,2,1:2:0:0: -148,116,12494,1,10,0:0:0:0: -100,248,12659,1,2,0:0:0:0: -268,192,12824,5,0,1:0:0:0: -268,192,12906,1,0,0:0:0:0: -268,192,12988,1,0,0:0:0:0: -340,272,13153,2,0,P|384:276|432:264,1,90,8|0,0:0|1:0,0:0:0:0: -452,244,13401,1,0,0:0:0:0: -468,216,13483,2,0,L|476:124,1,90,0|0,0:0|1:0,0:0:0:0: -368,32,13813,2,0,L|360:121,1,90,8|0,0:0|0:0,0:0:0:0: -340,272,14142,6,0,L|316:316,2,45,0|0|0,1:0|0:0|0:0,0:0:0:0: -452,244,14472,1,8,0:0:0:0: -268,192,14637,1,0,0:0:0:0: -236,188,14719,1,0,0:0:0:0: -204,192,14802,2,0,P|172:228|160:272,1,90,0|0,0:0|1:0,0:0:0:0: -128,140,15131,2,0,P|160:104|172:60,1,90,8|0,0:0|0:0,0:0:0:0: -64,52,15461,6,0,L|20:68,2,45,0|0|0,1:0|0:0|0:0,0:0:0:0: -171,64,15791,1,8,0:0:0:0: -264,8,15956,2,0,L|356:12,1,90,0|0,1:0|0:0,0:0:0:0: -452,56,16285,1,0,1:0:0:0: -296,140,16450,2,0,L|206:136,1,90,8|0,0:0|0:0,0:0:0:0: -108,184,16780,6,0,P|92:224|96:272,1,90,0|0,1:0|0:0,0:0:0:0: -200,244,17109,1,8,0:0:0:0: -108,108,17274,2,0,L|12:116,1,90,0|0,0:0|0:0,0:0:0:0: -200,244,17604,1,0,1:0:0:0: -296,140,17769,2,0,L|385:132,1,90,8|0,0:0|0:0,0:0:0:0: -480,184,18098,5,0,1:0:0:0: -488,216,18181,1,0,0:0:0:0: -496,248,18263,2,0,L|492:340,1,90,0|8,0:0|0:0,0:0:0:0: -404,224,18593,2,0,L|396:176,2,45,0|0|0,1:0|0:0|0:0,0:0:0:0: -304,264,18923,1,0,1:0:0:0: -200,244,19087,2,0,P|156:240|108:248,1,90,8|0,0:0|0:0,0:0:0:0: -296,140,19417,6,0,P|340:144|388:136,1,90,0|0,1:0|0:0,0:0:0:0: -440,44,19747,1,8,0:0:0:0: -404,224,19912,1,0,0:0:0:0: -404,224,19994,1,0,0:0:0:0: -404,224,20076,2,0,L|412:320,1,90,0|0,0:0|1:0,0:0:0:0: -200,244,20406,2,0,L|192:154,1,90,8|0,0:0|0:0,0:0:0:0: -184,44,20736,5,4,1:2:0:0: -152,40,20818,1,0,0:0:0:0: -120,48,20901,1,0,0:0:0:0: -96,68,20983,1,0,0:0:0:0: -76,92,21065,1,2,0:3:0:0: -64,120,21148,1,0,0:0:0:0: -60,152,21230,1,0,1:0:0:0: -64,184,21313,1,0,0:0:0:0: -76,212,21395,2,0,L|96:252,3,45,0|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -144,316,21725,2,0,L|188:324,3,45,0|0|2|0,0:0|0:0|0:3|0:0,0:0:0:0: -268,340,22054,6,0,L|364:336,1,90,4|0,1:2|0:0,0:0:0:0: -452,280,22384,1,8,0:0:0:0: -512,188,22549,2,0,P|516:144|504:96,1,90,2|0,0:0|0:0,0:0:0:0: -340,24,22879,2,0,P|336:68|348:116,1,90,2|8,1:2|0:0,0:0:0:0: -420,192,23208,1,2,0:0:0:0: -328,252,23373,6,0,L|232:240,1,90,0|0,1:0|0:0,0:0:0:0: -64,256,23703,1,8,0:0:0:0: -144,184,23868,2,0,P|148:140|136:88,1,90,0|0,1:0|0:0,0:0:0:0: -40,52,24197,1,2,1:2:0:0: -139,95,24362,1,8,0:0:0:0: -216,20,24527,1,0,0:0:0:0: -315,63,24692,6,0,P|360:72|408:68,1,90,2|0,1:2|0:0,0:0:0:0: -492,132,25021,1,8,0:0:0:0: -412,204,25186,2,0,P|403:249|407:297,1,90,2|0,0:0|0:0,0:0:0:0: -268,328,25516,2,0,P|277:283|273:235,1,90,2|8,1:2|0:0,0:0:0:0: -232,140,25846,2,0,P|187:131|139:135,1,90,2|0,0:0|1:0,0:0:0:0: -64,208,26175,5,2,0:0:0:0: -44,316,26340,1,8,0:0:0:0: -148,280,26505,1,2,1:2:0:0: -456,208,26835,1,2,1:2:0:0: -476,316,26999,1,10,0:0:0:0: -372,280,27164,1,2,0:0:0:0: -356,172,27329,6,0,L|380:80,1,90,0|0,1:0|0:0,0:0:0:0: -456,208,27659,1,8,0:0:0:0: -300,236,27824,1,2,0:0:0:0: -300,236,27906,1,0,0:0:0:0: -300,236,27988,2,0,L|208:228,1,90,0|2,0:0|1:2,0:0:0:0: -140,312,28318,1,8,0:0:0:0: -372,280,28483,2,0,L|464:272,1,90,2|0,0:0|1:0,0:0:0:0: -500,136,28813,5,2,0:0:0:0: -432,56,28977,1,8,0:0:0:0: -328,24,29142,2,0,P|284:24|236:28,1,90,2|0,1:2|0:0,0:0:0:0: -80,144,29472,1,2,1:2:0:0: -116,44,29637,1,10,0:0:0:0: -184,128,29802,1,2,0:0:0:0: -20,88,29966,6,0,P|1:164|73:227,1,180,2|10,1:2|0:0,0:0:0:0: -184,128,30461,2,0,P|227:120|276:124,1,90,2|0,0:0|0:0,0:0:0:0: -392,188,30791,1,2,1:2:0:0: -272,260,30956,1,8,0:0:0:0: -396,328,31120,1,0,0:0:0:0: -256,348,31285,5,2,1:2:0:0: -224,344,31368,1,0,1:0:0:0: -192,340,31450,2,0,L|172:248,1,90,2|0,1:2|1:0,0:0:0:0: -8,136,31780,2,0,L|27:223,1,90,2|0,1:2|0:0,0:0:0:0: -56,328,32109,1,2,1:2:0:0: -108,192,32274,1,2,1:2:0:0: -100,160,32357,1,0,1:0:0:0: -92,132,32439,1,2,1:2:0:0: -84,104,32521,1,0,1:0:0:0: -76,72,32604,6,0,P|100:112|148:136,1,90,4|0,1:2|0:0,0:0:0:0: -240,168,32934,1,8,0:0:0:0: -336,124,33098,2,0,L|344:80,2,45,2|0|0,0:0|0:0|0:0,0:0:0:0: -264,248,33428,2,0,P|220:248|176:220,1,90,2|8,1:2|0:0,0:0:0:0: -260,84,33758,1,2,0:0:0:0: -344,212,33923,5,0,1:0:0:0: -344,212,34005,1,0,0:0:0:0: -344,212,34087,1,0,0:0:0:0: -440,160,34252,1,8,0:0:0:0: -312,320,34417,2,0,P|272:336|220:324,1,90,0|0,1:0|0:0,0:0:0:0: -156,176,34747,2,0,P|196:160|248:172,2,90,2|8|0,1:2|0:0|0:0,0:0:0:0: -132,280,35241,5,2,1:2:0:0: -132,280,35324,1,0,0:0:0:0: -132,280,35406,2,0,L|120:376,1,90,0|8,0:0|0:0,0:0:0:0: -312,320,35736,2,0,L|300:230,1,90,2|0,0:0|0:0,0:0:0:0: -316,124,36065,1,2,1:2:0:0: -400,192,36230,1,8,0:0:0:0: -300,230,36395,2,0,P|255:231|211:224,1,90,2|0,0:0|1:0,0:0:0:0: -24,132,36725,5,0,0:0:0:0: -132,152,36890,1,8,0:0:0:0: -60,232,37054,1,2,1:2:0:0: -60,232,37137,1,0,0:0:0:0: -60,232,37219,1,0,0:0:0:0: -92,56,37384,2,0,L|184:44,1,90,2|10,1:2|0:0,0:0:0:0: -316,124,37714,2,0,L|226:135,1,90,2|0,0:0|1:0,0:0:0:0: -60,232,38043,6,0,P|52:276|64:328,1,90,0|8,0:0|0:0,0:0:0:0: -220,152,38373,2,0,P|176:144|124:156,1,90,2|0,0:0|0:0,0:0:0:0: -176,252,38703,1,2,1:2:0:0: -323,213,38868,2,0,L|316:124,1,90,8|2,0:0|0:0,0:0:0:0: -332,320,39197,5,0,1:0:0:0: -424,260,39362,1,2,0:0:0:0: -260,272,39527,2,0,P|246:313|256:360,1,90,8|2,0:0|1:2,0:0:0:0: -408,336,39857,1,0,0:0:0:0: -176,252,40021,2,0,L|80:260,2,90,2|10|2,1:2|0:0|0:0,0:0:0:0: -324,212,40516,5,2,1:2:0:0: -324,212,40598,1,0,1:0:0:0: -324,212,40681,1,0,1:0:0:0: -200,336,40846,1,2,1:2:0:0: -236,188,41010,1,2,1:2:0:0: -236,188,41093,1,0,1:0:0:0: -236,188,41175,1,0,1:0:0:0: -281,357,41340,1,2,1:2:0:0: -176,252,41505,1,2,1:2:0:0: -176,252,41587,1,0,1:0:0:0: -176,252,41670,1,0,1:0:0:0: -344,297,41835,5,2,1:2:0:0: -432,232,41999,1,2,1:2:0:0: -444,204,42082,1,0,1:0:0:0: -448,172,42164,1,0,1:0:0:0: -444,140,42247,1,0,1:0:0:0: -432,112,42329,2,0,L|440:64,2,45,2|0|0,1:2|1:0|1:0,0:0:0:0: -236,188,42659,1,0,0:0:0:0: -340,172,42824,1,2,0:3:0:0: -272,88,42988,1,0,0:0:0:0: -132,160,43153,6,0,P|148:248|220:296,1,180,4|8,1:2|0:0,0:0:0:0: -324,320,43648,2,0,L|336:364,2,45,0|0|0,0:0|0:0|0:0,0:0:0:0: -292,216,43977,1,0,1:0:0:0: -396,240,44142,2,0,P|440:244|488:232,1,90,8|0,0:0|0:0,0:0:0:0: -328,124,44472,6,0,P|284:120|236:132,1,90,0|0,1:0|0:0,0:0:0:0: -168,212,44802,1,8,0:0:0:0: -192,316,44966,1,0,1:0:0:0: -140,220,45131,1,0,0:0:0:0: -83,310,45296,1,0,1:0:0:0: -114,205,45461,1,8,0:0:0:0: -10,229,45626,1,0,0:0:0:0: -106,176,45791,6,0,P|113:133|108:85,1,90,0|0,1:0|0:0,0:0:0:0: -204,136,46120,1,8,0:0:0:0: -256,40,46285,1,0,0:0:0:0: -256,40,46368,1,0,0:0:0:0: -256,40,46450,2,0,L|356:44,1,90,0|0,0:0|1:0,0:0:0:0: -501,124,46780,2,0,L|412:128,1,90,8|0,0:0|0:0,0:0:0:0: -324,192,47109,5,0,1:0:0:0: -356,296,47274,1,0,0:0:0:0: -284,216,47439,1,8,0:0:0:0: -269,323,47604,1,0,1:0:0:0: -237,220,47769,1,0,0:0:0:0: -178,311,47934,1,0,1:0:0:0: -191,203,48098,1,8,0:0:0:0: -99,261,48263,1,0,0:0:0:0: -156,168,48428,6,0,B|176:112|136:64|136:64|200:96,1,180,4|8,1:2|0:0,0:0:0:0: -300,124,48923,2,0,L|392:120,1,90,0|0,0:0|0:0,0:0:0:0: -468,48,49252,1,0,1:0:0:0: -390,120,49417,2,0,P|390:164|406:208,1,90,8|0,0:0|0:0,0:0:0:0: -352,344,49747,6,0,P|352:300|336:256,1,90,4|0,1:2|0:0,0:0:0:0: -240,208,50076,1,8,0:0:0:0: -163,320,50241,2,0,P|207:324|252:316,1,90,0|0,1:0|0:0,0:0:0:0: -240,208,50571,1,0,1:0:0:0: -76,296,50736,2,0,P|76:340|92:384,1,90,8|0,0:0|0:0,0:0:0:0: -312,164,51065,6,0,P|236:124|160:184,1,180,4|8,1:2|0:0,0:0:0:0: -247,297,51560,2,0,L|240:208,1,90,0|0,0:0|0:0,0:0:0:0: -224,48,51890,1,0,1:0:0:0: -332,56,52054,2,0,L|366:58,5,30,8|0|0|0|0|0,0:0|0:0|0:0|0:0|0:0|0:0,0:0:0:0: -408,64,52384,6,0,P|420:108|416:156,1,90,0|0,1:0|0:0,0:0:0:0: -360,260,52714,1,8,0:0:0:0: -247,297,52879,2,0,B|203:281|159:297|159:297|115:313|71:297,1,180,0|0,1:0|1:0,0:0:0:0: -116,196,53373,1,8,0:0:0:0: -120,164,53456,1,0,0:0:0:0: -124,132,53538,1,0,0:0:0:0: -128,100,53620,1,0,0:0:0:0: -132,68,53703,5,4,1:2:0:0: -40,136,53868,1,0,0:0:0:0: -204,160,54032,2,0,L|304:152,1,90,8|0,0:0|0:0,0:0:0:0: -408,64,54362,1,0,0:0:0:0: -408,64,54445,1,0,0:0:0:0: -408,64,54527,2,0,P|404:112|416:160,1,90,0|8,1:0|0:0,0:0:0:0: -484,236,54857,1,0,0:0:0:0: -428,328,55021,5,0,1:0:0:0: -328,296,55186,1,0,0:0:0:0: -328,296,55269,1,0,0:0:0:0: -328,296,55351,1,8,0:0:0:0: -416,300,55516,1,0,1:0:0:0: -472,208,55681,1,0,0:0:0:0: -316,268,55846,1,0,1:0:0:0: -460,180,56010,1,8,0:0:0:0: -304,240,56175,1,0,0:0:0:0: -404,272,56340,5,0,1:0:0:0: -448,152,56505,1,0,0:0:0:0: -448,152,56587,1,0,0:0:0:0: -448,152,56670,2,0,P|456:112|448:60,1,90,8|0,0:0|0:0,0:0:0:0: -268,28,56999,2,0,P|260:68|268:120,1,90,0|0,0:0|1:0,0:0:0:0: -404,272,57329,2,0,P|444:280|496:272,2,90,8|0|0,0:0|0:0|1:0,0:0:0:0: -304,240,57824,5,0,0:0:0:0: -252,336,57988,1,8,0:0:0:0: -196,244,58153,1,0,1:0:0:0: -24,256,58318,1,0,0:0:0:0: -116,200,58483,1,0,1:0:0:0: -136,60,58648,1,8,0:0:0:0: -192,152,58813,1,0,0:0:0:0: -304,240,58977,6,0,P|348:252|396:248,1,90,0|0,1:0|0:0,0:0:0:0: -456,116,59307,2,0,P|412:104|364:108,1,90,8|0,0:0|0:0,0:0:0:0: -273,161,59637,1,0,0:0:0:0: -136,60,59802,1,0,1:0:0:0: -192,152,59966,1,8,0:0:0:0: -23,177,60131,1,0,0:0:0:0: -129,203,60296,5,0,1:0:0:0: -88,304,60461,2,0,P|132:311|176:303,1,90,0|8,0:0|0:0,0:0:0:0: -304,240,60791,1,0,1:0:0:0: -304,240,60873,1,0,0:0:0:0: -304,240,60956,2,0,L|312:288,3,45,0|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -384,256,61285,2,0,L|392:304,3,45,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -464,272,61615,5,2,1:2:0:0: -488,168,61780,1,2,0:0:0:0: -428,80,61945,1,10,0:0:0:0: -332,32,62109,2,0,P|288:28|240:36,1,90,2|0,0:0|0:0,0:0:0:0: -28,216,62439,1,2,1:2:0:0: -88,304,62604,1,10,0:0:0:0: -184,352,62769,2,0,P|228:356|276:348,1,90,2|0,0:0|1:0,0:0:0:0: -384,256,63098,6,0,P|409:219|426:174,1,90,2|8,0:0|0:0,0:0:0:0: -428,80,63428,2,0,L|420:36,2,45,2|0|0,1:2|0:0|0:0,0:0:0:0: -456,288,63758,1,2,1:2:0:0: -324,200,63923,1,10,1:2:0:0: -292,204,64005,1,0,1:0:0:0: -260,208,64087,1,2,1:2:0:0: -228,212,64170,1,0,1:0:0:0: -196,216,64252,5,4,1:2:0:0: -104,160,64417,1,0,0:0:0:0: -228,296,64582,2,0,L|320:284,1,90,8|0,0:0|0:0,0:0:0:0: -344,112,64912,1,0,0:0:0:0: -344,112,64994,1,0,0:0:0:0: -344,112,65076,2,0,L|254:123,1,90,0|8,1:0|0:0,0:0:0:0: -144,284,65406,2,0,P|148:328|176:364,1,90,0|0,0:0|1:0,0:0:0:0: -196,216,65736,5,0,0:0:0:0: -196,216,65818,1,0,0:0:0:0: -196,216,65901,2,0,P|155:198|110:205,1,90,8|0,0:0|1:0,0:0:0:0: -36,284,66230,1,0,0:0:0:0: -4,180,66395,1,0,1:0:0:0: -132,24,66560,1,8,0:0:0:0: -100,128,66725,1,0,0:0:0:0: -24,48,66890,5,0,1:0:0:0: -212,108,67054,1,0,0:0:0:0: -212,108,67137,1,0,0:0:0:0: -212,108,67219,2,0,L|300:92,1,90,8|0,0:0|0:0,0:0:0:0: -472,144,67549,2,0,L|384:160,1,90,0|0,0:0|1:0,0:0:0:0: -196,216,67879,2,0,P|240:216|288:240,1,90,8|0,0:0|0:0,0:0:0:0: -324,336,68208,5,0,1:0:0:0: -144,288,68373,1,0,0:0:0:0: -58,170,68538,1,8,0:0:0:0: -196,215,68703,1,0,1:0:0:0: -58,260,68868,1,0,0:0:0:0: -144,142,69032,2,0,L|138:108,2,30,0|0|0,1:0|0:0|0:0,0:0:0:0: -144,142,69197,2,0,P|184:124|232:132,1,90,8|0,0:0|0:0,0:0:0:0: -312,248,69527,6,0,L|324:338,1,90,0|0,1:0|0:0,0:0:0:0: -436,248,69857,1,8,0:0:0:0: -432,216,69939,1,0,0:0:0:0: -428,184,70021,1,0,0:0:0:0: -328,120,70186,1,0,0:0:0:0: -324,152,70269,1,0,0:0:0:0: -320,184,70351,1,0,1:0:0:0: -316,216,70434,1,0,0:0:0:0: -312,248,70516,2,0,L|320:300,1,45,8|0,0:0|0:0,0:0:0:0: -244,340,70681,2,0,L|237:295,1,45,0|0,0:0|0:0,0:0:0:0: -216,224,70846,6,0,P|168:216|124:224,1,90,0|0,1:0|0:0,0:0:0:0: -40,288,71175,1,8,0:0:0:0: -2,95,71340,2,0,P|-4:139|4:184,1,90,0|0,1:0|0:0,0:0:0:0: -164,304,71670,1,0,1:0:0:0: -312,248,71835,1,8,0:0:0:0: -244,340,71999,1,0,0:0:0:0: -216,224,72164,6,0,L|228:132,1,90,0|0,1:0|0:0,0:0:0:0: -332,148,72494,2,0,L|344:56,1,90,8|0,0:0|0:0,0:0:0:0: -312,248,72824,1,0,0:0:0:0: -164,304,72988,1,0,1:0:0:0: -332,336,73153,1,8,0:0:0:0: -360,324,73236,1,0,0:0:0:0: -384,304,73318,1,0,0:0:0:0: -399,276,73401,1,0,0:0:0:0: -403,244,73483,6,0,L|396:200,3,45,4|0|2|0,1:2|0:0|0:0|1:0,0:0:0:0: -420,112,73813,2,0,L|427:68,3,45,2|0|2|0,1:2|0:0|1:2|0:0,0:0:0:0: -352,16,74142,2,0,L|345:60,3,45,0|0|2|0,0:0|1:0|1:2|0:0,0:0:0:0: -332,148,74472,1,2,1:2:0:0: -332,148,74554,1,0,1:0:0:0: -332,148,74637,1,2,1:2:0:0: -332,148,74719,1,0,1:0:0:0: -332,148,74802,6,0,P|360:216|320:312,1,180,4|2,1:2|0:3,0:0:0:0: -190,310,75296,2,0,P|151:231|180:148,1,180,4|0,1:2|0:0,0:0:0:0: -256,56,75791,1,0,0:0:0:0: -332,148,75956,1,2,0:3:0:0: -179,148,76120,5,4,1:2:0:0: -336,64,76285,1,4,1:2:0:0: -256,224,76450,1,2,0:3:0:0: -176,64,76615,1,4,1:2:0:0: -256,140,76780,2,0,L|256:324,1,180,2|0,0:0|0:0,0:0:0:0: -364,300,77274,1,2,0:3:0:0: -148,300,77439,6,0,P|104:316|76:356,1,90,4|0,1:2|0:0,0:0:0:0: -24,252,77769,2,0,L|16:208,3,45,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -96,212,78098,2,0,L|104:168,3,45,0|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -32,128,78428,2,0,L|24:84,3,45,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -104,88,78758,5,2,1:2:0:0: -204,132,78923,1,0,0:0:0:0: -236,124,79005,1,0,0:0:0:0: -268,116,79087,2,0,L|280:68,3,45,8|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -348,100,79417,2,0,L|360:52,3,45,0|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -428,84,79747,1,8,1:2:0:0: -460,76,79829,1,0,1:0:0:0: -492,68,79912,1,0,1:0:0:0: -492,260,80076,6,0,P|400:248|328:296,1,180,4|2,1:2|0:3,0:0:0:0: -144,236,80571,2,0,P|236:248|308:200,1,180,4|0,1:2|0:0,0:0:0:0: -348,100,81065,2,0,P|348:56|336:8,1,90,0|2,0:0|0:3,0:0:0:0: -140,48,81395,5,4,1:2:0:0: -244,68,81560,1,4,1:2:0:0: -144,236,81725,1,2,0:3:0:0: -176,133,81890,1,4,1:2:0:0: -184,304,82054,2,0,P|100:300|68:220,1,180,2|0,0:0|0:0,0:0:0:0: -100,116,82549,1,2,0:3:0:0: -264,244,82714,6,0,L|272:340,1,90,4|0,1:2|0:0,0:0:0:0: -380,316,83043,1,8,0:0:0:0: -396,288,83126,1,0,0:0:0:0: -400,256,83208,1,0,0:0:0:0: -396,224,83291,1,0,0:0:0:0: -380,196,83373,2,0,L|336:176,3,45,0|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -272,148,83703,1,8,0:0:0:0: -256,120,83785,1,0,0:0:0:0: -252,88,83868,1,0,0:0:0:0: -256,56,83950,1,0,0:0:0:0: -272,28,84032,6,0,L|316:8,3,45,2|0|0|0,1:2|0:0|0:0|0:0,0:0:0:0: -360,72,84362,2,0,L|408:72,3,45,8|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -421,149,84692,2,0,L|464:169,3,45,2|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -443,244,85021,2,0,L|473:281,3,45,8|0|0|0,1:2|1:0|0:0|0:0,0:0:0:0: -422,339,85351,6,0,L|240:348,1,180,4|2,1:2|0:3,0:0:0:0: -76,172,85846,2,0,L|255:163,1,180,4|0,1:2|0:0,0:0:0:0: -421,149,86340,2,0,P|435:107|428:56,1,90,0|2,0:0|0:3,0:0:0:0: -228,56,86670,5,4,1:2:0:0: -280,192,86835,1,4,1:2:0:0: -328,96,86999,1,2,0:3:0:0: -180,152,87164,1,4,1:2:0:0: -28,100,87330,2,0,P|16:56|20:8,1,90,2|0,0:0|0:0,0:0:0:0: -0,180,87659,1,0,0:0:0:0: -28,284,87824,1,2,0:3:0:0: -108,352,87988,6,0,P|152:360|196:356,1,90,4|0,1:2|0:0,0:0:0:0: -276,284,88318,1,8,0:0:0:0: -304,272,88401,1,0,0:0:0:0: -336,268,88483,1,0,0:0:0:0: -368,272,88565,1,0,0:0:0:0: -396,284,88648,2,0,L|432:312,1,45,0|0,0:0|0:0,0:0:0:0: -488,252,88813,2,0,L|452:224,1,45,0|0,1:0|0:0,0:0:0:0: -400,164,88977,2,0,L|396:116,3,45,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -316,64,89307,6,0,L|320:160,1,90,2|0,1:2|0:0,0:0:0:0: -276,284,89637,1,8,0:0:0:0: -248,296,89719,1,0,0:0:0:0: -216,300,89802,1,0,1:0:0:0: -184,296,89884,1,0,0:0:0:0: -156,284,89966,2,0,L|120:256,1,45,0|0,0:0|0:0,0:0:0:0: -176,200,90131,2,0,L|140:172,1,45,0|0,1:0|0:0,0:0:0:0: -196,116,90296,2,0,L|160:88,3,45,8|0|0|0,1:2|1:0|1:0|0:0,0:0:0:0: -92,44,90626,6,0,P|48:44|24:160,1,180,4|2,1:2|0:3,0:0:0:0: -156,284,91120,2,0,B|200:300|244:284|244:284|288:268|332:284,1,180,4|0,1:2|0:0,0:0:0:0: -176,200,91615,2,0,P|176:156|196:116,1,90,0|2,0:0|0:3,0:0:0:0: -264,28,91945,6,0,L|353:39,1,90,4|0,1:2|1:0,0:0:0:0: -453,159,92274,2,0,L|364:148,1,90,2|4,0:3|1:2,0:0:0:0: -268,196,92604,2,0,P|260:268|328:348,1,180,2|0,0:0|0:0,0:0:0:0: -364,248,93098,1,2,0:3:0:0: -176,200,93263,5,4,1:2:0:0: -72,228,93428,1,0,1:0:0:0: -152,92,93593,1,0,1:0:0:0: -256,64,93758,1,0,1:0:0:0: -336,200,93923,5,0,1:0:0:0: -440,228,94087,1,0,1:0:0:0: -360,92,94252,1,0,1:0:0:0: -256,64,94417,1,0,1:0:0:0: -176,200,94582,5,2,1:2:0:0: -168,228,94664,1,0,1:0:0:0: -168,260,94747,1,0,1:0:0:0: -172,292,94829,1,0,1:0:0:0: -192,316,94912,1,0,1:0:0:0: -220,328,94994,1,0,1:0:0:0: -252,332,95076,1,0,1:0:0:0: -280,320,95159,1,0,1:0:0:0: -300,296,95241,2,0,L|308:248,3,45,2|0|0|0,1:2|1:0|1:0|1:0,0:0:0:0: -312,172,95571,2,0,L|304:127,3,45,0|0|0|0,1:0|1:0|1:0|1:0,0:0:0:0: -256,64,95901,6,0,P|208:56|164:60,1,90,4|0,1:2|0:0,0:0:0:0: -76,116,96230,1,8,0:0:0:0: -60,224,96395,1,0,0:0:0:0: -60,224,96477,1,0,0:0:0:0: -160,184,96642,1,0,0:0:0:0: -160,184,96725,1,0,1:0:0:0: -63,26,96890,2,0,L|76:116,1,90,8|0,0:0|0:0,0:0:0:0: -136,272,97219,5,0,1:0:0:0: -168,268,97302,1,0,0:0:0:0: -200,264,97384,1,0,0:0:0:0: -232,260,97466,1,0,0:0:0:0: -264,256,97549,1,8,0:0:0:0: -384,136,97714,1,0,1:0:0:0: -376,168,97796,1,0,0:0:0:0: -380,200,97879,1,0,0:0:0:0: -392,228,97961,1,0,0:0:0:0: -416,248,98043,2,0,P|464:260|512:260,2,90,0|8|0,1:0|0:0|0:0,0:0:0:0: -231,105,98538,6,0,L|188:116,2,45,0|0|0,1:0|0:0|0:0,0:0:0:0: -376,56,98868,2,0,L|420:64,1,45,8|0,0:0|0:0,0:0:0:0: -384,136,99032,1,0,0:0:0:0: -384,136,99115,2,0,P|340:128|304:92,1,90,0|0,0:0|0:0,0:0:0:0: -303,18,99362,2,0,L|207:26,2,90,0|8|0,1:0|0:0|0:0,0:0:0:0: -452,88,99857,5,0,1:0:0:0: -465,116,99939,1,0,0:0:0:0: -466,147,100021,1,0,0:0:0:0: -456,177,100104,1,0,0:0:0:0: -436,201,100186,2,0,P|416:213|389:216,3,45,8|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -320,188,100516,2,0,P|300:176|273:173,3,45,0|0|0|0,0:0|1:0|1:0|0:0,0:0:0:0: -204,200,100846,2,0,P|192:220|189:247,3,45,8|0|0|0,0:0|0:0|1:0|0:0,0:0:0:0: -188,320,101175,6,0,P|143:322|100:310,1,90,0|0,1:0|0:0,0:0:0:0: -76,292,101423,1,0,0:0:0:0: -76,292,101505,1,8,0:0:0:0: -76,292,101587,2,0,L|72:248,1,45 -12,68,101835,2,0,L|6:24,2,45,0|0|0,0:0|0:0|1:0,0:0:0:0: -104,140,102164,2,0,L|171:132,3,45,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -224,124,102494,6,0,P|236:164|232:216,1,90,0|0,1:0|0:0,0:0:0:0: -288,296,102824,1,8,0:0:0:0: -288,296,102906,1,0,0:0:0:0: -288,296,102988,2,0,P|328:284|380:288,1,90,0|0,1:0|0:0,0:0:0:0: -404,304,103236,1,0,0:0:0:0: -424,328,103318,1,0,1:0:0:0: -448,188,103483,2,0,L|440:140,3,45,8|0|0|0,0:0|0:0|0:0|0:0,0:0:0:0: -424,72,103813,5,0,1:0:0:0: -324,112,103977,1,0,0:0:0:0: -324,112,104060,1,0,0:0:0:0: -324,112,104142,2,0,P|280:116|232:104,1,90,8|0,0:0|0:0,0:0:0:0: -160,28,104472,1,0,0:0:0:0: -216,208,104637,1,0,1:0:0:0: -216,208,104719,1,0,0:0:0:0: -216,208,104802,1,8,0:0:0:0: -352,240,104966,1,0,0:0:0:0: -384,244,105049,1,0,0:0:0:0: -416,248,105131,6,0,L|460:240,4,45,0|0|0|0|8,1:0|0:0|0:0|0:0|0:0,0:0:0:0: -272,288,105626,1,0,1:0:0:0: -264,320,105708,1,0,0:0:0:0: -256,352,105791,2,0,L|204:356,5,30,0|0|0|0|0|0,0:0|0:0|0:0|1:0|0:0|0:0,0:0:0:0: -156,332,106120,2,0,L|104:336,5,30,8|0|0|0|0|0,0:0|0:0|0:0|1:0|0:0|0:0,0:0:0:0: -56,312,106450,5,4,1:2:0:0: -4,188,106615,1,0,0:0:0:0: -168,220,106780,2,0,P|127:232|79:228,1,90,8|0,0:0|0:0,0:0:0:0: -112,124,107109,1,0,0:0:0:0: -272,216,107274,2,0,L|264:316,1,90,0|8,1:0|0:0,0:0:0:0: -400,268,107604,1,0,0:0:0:0: -428,132,107769,5,0,1:0:0:0: -428,132,107851,1,0,0:0:0:0: -428,132,107934,1,0,0:0:0:0: -428,132,108016,1,0,0:0:0:0: -428,132,108098,1,8,0:0:0:0: -332,84,108263,2,0,P|288:80|232:88,1,90,0|0,1:0|0:0,0:0:0:0: -112,124,108593,1,0,1:0:0:0: -148,264,108758,1,8,0:0:0:0: -16,236,108923,1,0,0:0:0:0: -264,126,109087,6,0,L|272:216,1,90,0|0,1:0|0:0,0:0:0:0: -452,224,109417,2,0,L|460:320,1,90,8|0,0:0|0:0,0:0:0:0: -360,232,109747,1,0,0:0:0:0: -348,56,109912,1,0,1:0:0:0: -416,140,110076,1,8,0:0:0:0: -256,112,110241,2,0,P|212:120|160:112,1,90,0|0,0:0|1:0,0:0:0:0: -348,56,110571,6,0,L|331:150,1,90,0|8,0:0|0:0,0:0:0:0: -208,328,110901,2,0,L|191:239,1,90,0|0,1:0|0:0,0:0:0:0: -184,216,111148,1,0,1:0:0:0: -178,194,111230,1,0,1:0:0:0: -68,272,111395,1,8,0:0:0:0: -56,136,111560,1,0,1:0:0:0: -178,194,111725,6,0,P|219:203|267:199,1,90,4|0,1:2|0:0,0:0:0:0: -364,148,112054,1,8,0:0:0:0: -384,256,112219,2,0,P|406:291|443:322,1,90,0|0,0:0|0:0,0:0:0:0: -488,224,112549,1,0,1:0:0:0: -304,232,112714,2,0,L|208:224,2,90,8|0|0,0:0|0:0|1:0,0:0:0:0: -208,328,113208,6,0,L|112:320,1,90,0|8,0:0|0:0,0:0:0:0: -26,184,113538,2,0,L|116:192,1,90,0|0,1:0|0:0,0:0:0:0: -304,232,113868,1,0,1:0:0:0: -116,192,114032,1,8,0:0:0:0: -224,132,114197,1,0,0:0:0:0: -208,328,114362,6,0,B|272:360|320:312|320:312|340:368,1,180,4|8,1:2|0:0,0:0:0:0: -304,232,114857,2,0,P|300:184|308:140,1,90,0|0,0:0|0:0,0:0:0:0: -384,64,115186,1,0,1:0:0:0: -307,143,115351,1,8,0:0:0:0: -256,48,115516,1,0,0:0:0:0: -456,24,115681,6,0,B|482:101|420:136|420:136|440:184,1,180,4|8,1:2|0:0,0:0:0:0: -384,64,116175,2,0,P|340:56|296:64,1,90,0|0,1:0|0:0,0:0:0:0: -211,171,116505,1,0,1:0:0:0: -439,181,116670,2,0,L|448:84,1,90,8|0,0:0|0:0,0:0:0:0: -372,296,116999,6,2,L|304:292,1,67.5000025749208,2|0,0:1|0:0,0:0:0:0: -136,252,117329,6,2,P|196:260|212:172,1,168.75,0|0,0:0|0:0,0:0:0:0: -192,148,117659,1,2,0:3:0:0: -164,132,117741,1,2,0:3:0:0: -132,124,117824,1,2,1:3:0:0: -100,132,117906,1,2,0:3:0:0: -72,148,117988,2,0,L|52:56,1,90,2|8,0:3|0:0,0:0:0:0: -36,244,118318,5,0,1:0:0:0: -76,344,118483,1,0,1:0:0:0: -184,352,118648,1,0,1:0:0:0: -244,264,118813,1,0,1:0:0:0: -244,264,118895,1,0,1:0:0:0: -244,264,118977,2,0,L|288:260,3,45,2|0|2|0,1:2|0:0|0:0|0:0,0:0:0:0: -332,328,119307,2,0,L|376:324,3,45,2|0|2|0,1:2|0:0|0:0|0:0,0:0:0:0: -412,252,119637,5,4,1:2:0:0: -256,192,119719,12,0,122274,0:0:0:0: -256,192,140735,6,0,L|228:156,1,45,4|0,1:2|0:0,0:0:0:0: -152,132,141065,2,0,P|129:129|104:136,1,45 -48,192,141395,2,0,P|40:236|52:280,1,90,8|8,0:0|0:0,0:0:0:0: -196,352,142054,6,0,L|308:340,1,90,8|8,0:0|1:2,0:0:0:0: -336,280,142549,1,0,0:0:0:0: -404,324,142713,1,8,0:0:0:0: -404,324,142878,1,8,0:0:0:0: -292,120,143373,5,0,1:0:0:0: -212,104,143538,1,0,0:0:0:0: -140,140,143702,1,0,0:0:0:0: -120,220,143867,1,0,0:0:0:0: -144,296,144032,2,0,P|184:320|228:316,1,90,10|8,0:0|0:0,0:0:0:0: -372,212,144691,6,0,P|327:209|290:232,1,90,10|8,0:0|1:2,0:0:0:0: -348,288,145186,1,0,0:0:0:0: -452,220,145351,1,10,0:0:0:0: -452,220,145516,1,8,0:0:0:0: -328,36,146010,5,2,1:2:0:0: -264,88,146175,1,0,0:0:0:0: -184,108,146340,1,0,0:0:0:0: -104,88,146505,1,0,0:0:0:0: -44,36,146669,1,8,0:0:0:0: -44,36,146999,1,8,0:0:0:0: -44,36,147329,6,0,L|24:84,1,45,8|0,0:0|0:0,0:0:0:0: -52,156,147658,2,0,L|71:204,1,45,8|0,1:2|0:0,0:0:0:0: -144,236,147988,1,8,0:0:0:0: -144,236,148153,1,8,0:0:0:0: -316,64,148647,5,0,1:0:0:0: -380,116,148812,1,0,0:0:0:0: -408,192,148977,1,0,0:0:0:0: -380,268,149142,1,0,0:0:0:0: -316,320,149307,2,0,L|224:316,1,90,10|8,0:0|0:0,0:0:0:0: -64,248,149966,5,10,0:0:0:0: -144,236,150131,1,0,0:0:0:0: -188,168,150296,1,8,1:2:0:0: -192,88,150461,1,0,0:0:0:0: -140,24,150626,2,0,P|120:16|96:20,1,45,10|0,0:0|0:0,0:0:0:0: -260,132,150955,2,0,P|280:140|304:136,1,45,2|0,0:0|0:0,0:0:0:0: -476,48,151285,6,0,L|484:160,1,112.5,4|0,1:2|0:0,0:0:0:0: -464,236,151779,1,0,0:0:0:0: -436,308,151944,2,0,P|380:320|324:308,1,112.5,8|8,0:0|0:0,0:0:0:0: -76,308,152604,6,0,P|132:320|188:308,1,112.5,8|8,0:0|1:2,0:0:0:0: -256,88,153263,1,8,0:0:0:0: -256,168,153428,1,8,0:0:0:0: -256,168,153922,5,4,1:2:0:0: -256,248,154087,1,0,0:0:0:0: -324,128,154252,1,0,0:0:0:0: -188,128,154417,1,0,0:0:0:0: -332,212,154582,2,0,L|388:204,1,56.25,10|0,0:0|0:0,0:0:0:0: -492,152,154911,2,0,L|436:144,1,56.25,8|0,0:0|0:0,0:0:0:0: -324,128,155241,5,10,0:0:0:0: -180,212,155406,1,0,0:0:0:0: -332,212,155571,1,8,1:2:0:0: -188,128,155735,1,0,0:0:0:0: -256,248,155900,1,10,0:0:0:0: -256,248,156065,2,0,L|256:304,2,56.25,0|0|0,0:0|0:0|0:0,0:0:0:0: -180,212,156560,6,0,L|124:204,1,56.25,4|0,1:2|0:0,0:0:0:0: -20,152,156889,2,0,L|76:144,1,56.25,0|0,0:0|0:0,0:0:0:0: -188,128,157219,2,0,P|212:72|192:16,1,112.5,8|8,0:0|0:0,0:0:0:0: -132,72,157713,1,0,0:0:0:0: -180,212,157878,6,0,L|236:208,1,56.25,8|0,0:0|0:0,0:0:0:0: -360,252,158208,2,8,L|304:248,1,56.25,8|0,1:2|0:0,0:0:0:0: -168,292,158538,2,0,L|160:356,2,56.25,8|8|0,0:0|0:0|0:0,0:0:0:0: -180,212,159032,1,0,0:0:0:0: -144,140,159197,6,0,P|104:128|36:148,1,112.5,2|0,1:2|0:0,0:0:0:0: -12,220,159691,1,0,0:0:0:0: -36,296,159856,2,0,P|60:316|92:324,1,56.25,8|0,0:0|0:0,0:0:0:0: -215,264,160186,2,0,P|189:273|168:292,1,56.25,8|0,0:0|0:0,0:0:0:0: -228,344,160516,6,0,L|284:340,1,56.25,10|0,0:0|0:0,0:0:0:0: -328,276,160845,2,0,L|384:272,1,56.25,8|0,1:2|0:0,0:0:0:0: -428,208,161175,1,8,0:0:0:0: -440,128,161340,1,8,0:0:0:0: -400,60,161505,1,2,0:0:0:0: -328,28,161669,1,0,0:0:0:0: -212,76,161834,6,0,P|200:120|208:164,1,90,2|0,1:2|1:0,0:0:0:0: -300,308,162163,2,0,P|312:264|304:220,1,90,2|0,1:2|1:0,0:0:0:0: -140,236,162493,2,0,P|184:248|228:240,1,90,2|0,1:2|1:0,0:0:0:0: -372,148,162823,2,0,P|328:136|284:144,1,90,2|0,1:2|1:0,0:0:0:0: -104,316,163152,5,2,1:2:0:0: -78,297,163235,1,0,1:0:0:0: -60,270,163317,1,0,1:0:0:0: -54,239,163399,1,0,1:0:0:0: -58,207,163482,1,2,1:2:0:0: -74,180,163564,1,0,1:0:0:0: -98,159,163647,1,0,1:0:0:0: -127,149,163729,1,0,1:0:0:0: -158,150,163812,2,0,L|208:160,1,45,2|0,1:2|1:0,0:0:0:0: -344,184,163976,2,0,L|294:194,1,45,0|0,1:0|1:0,0:0:0:0: -140,236,164141,1,4,1:2:0:0: -140,236,164471,6,0,L|232:252,1,90,4|0,1:2|0:0,0:0:0:0: -344,184,164801,1,8,0:0:0:0: -380,284,164965,1,0,0:0:0:0: -368,104,165130,2,0,P|324:104|284:128,1,90,0|0,0:0|1:0,0:0:0:0: -356,360,165460,2,0,P|400:360|440:336,1,90,8|0,0:0|0:0,0:0:0:0: -432,208,165790,5,0,1:0:0:0: -292,260,165954,1,0,0:0:0:0: -344,184,166119,1,8,0:0:0:0: -204,236,166284,1,0,1:0:0:0: -204,236,166366,1,0,0:0:0:0: -204,236,166449,2,0,L|216:328,1,90,0|0,0:0|1:0,0:0:0:0: -120,208,166779,2,0,L|131:118,1,90,8|0,0:0|0:0,0:0:0:0: -204,236,167108,5,0,1:0:0:0: -32,216,167273,1,0,0:0:0:0: -130,118,167438,1,8,0:0:0:0: -110,298,167603,1,0,0:0:0:0: -110,298,167685,1,0,0:0:0:0: -110,298,167768,2,0,L|121:208,1,90,0|0,0:0|1:0,0:0:0:0: -304,40,168097,2,0,L|315:130,1,90,8|0,0:0|0:0,0:0:0:0: -328,236,168427,5,0,1:0:0:0: -184,148,168592,1,0,0:0:0:0: -314,129,168757,1,8,0:0:0:0: -197,254,168921,1,0,1:0:0:0: -197,254,169004,1,0,0:0:0:0: -197,254,169086,2,0,P|220:292|260:312,1,90,0|0,0:0|1:0,0:0:0:0: -409,210,169416,2,0,P|365:211|328:236,1,90,8|0,0:0|0:0,0:0:0:0: -488,232,169746,6,0,P|487:192|464:149,1,90,0|0,1:0|0:0,0:0:0:0: -314,129,170075,1,8,0:0:0:0: -409,210,170240,1,0,0:0:0:0: -332,40,170405,2,0,L|240:36,1,90,0|0,0:0|1:0,0:0:0:0: -68,144,170735,2,0,L|157:140,1,90,8|0,0:0|0:0,0:0:0:0: -314,129,171064,5,0,1:0:0:0: -332,40,171229,1,0,0:0:0:0: -324,216,171394,1,8,0:0:0:0: -306,305,171559,1,0,1:0:0:0: -257,178,171724,1,0,0:0:0:0: -168,160,171888,1,0,1:0:0:0: -384,164,172053,1,8,0:0:0:0: -473,182,172218,1,0,0:0:0:0: -306,305,172383,6,0,L|216:312,1,90,0|0,1:0|0:0,0:0:0:0: -60,172,172713,1,8,0:0:0:0: -120,260,172877,1,0,0:0:0:0: -168,160,173042,2,0,L|172:68,1,90,0|0,0:0|1:0,0:0:0:0: -309,216,173372,2,0,L|306:306,1,90,8|0,0:0|0:0,0:0:0:0: -120,260,173702,5,0,1:0:0:0: -152,256,173784,1,0,1:0:0:0: -184,252,173866,1,0,1:0:0:0: -309,216,174031,1,8,0:0:0:0: -103,168,174196,1,0,1:0:0:0: -135,164,174279,1,0,1:0:0:0: -167,160,174361,1,0,1:0:0:0: -292,124,174526,1,0,1:0:0:0: -87,76,174691,1,8,1:2:0:0: -119,72,174773,1,0,1:0:0:0: -151,68,174855,1,0,1:0:0:0: -276,32,175020,6,0,L|368:40,1,90,0|0,1:0|0:0,0:0:0:0: -448,108,175350,1,8,0:0:0:0: -292,124,175515,1,0,0:0:0:0: -292,124,175597,1,0,0:0:0:0: -292,124,175680,2,0,L|308:216,1,90,0|0,0:0|1:0,0:0:0:0: -328,320,176009,1,8,0:0:0:0: -408,248,176174,1,0,0:0:0:0: -220,300,176339,6,0,P|176:304|128:292,1,90,0|0,1:0|0:0,0:0:0:0: -16,120,176669,1,8,0:0:0:0: -120,152,176834,1,0,1:0:0:0: -120,152,176916,1,0,0:0:0:0: -120,152,176998,2,0,L|124:200,1,45 -212,176,177163,2,0,L|239:215,1,45,0|0,1:0|0:0,0:0:0:0: -292,124,177328,2,0,P|302:79|283:30,1,90,8|0,0:0|0:0,0:0:0:0: -344,192,177658,6,0,P|372:156|376:104,1,90,0|0,1:0|0:0,0:0:0:0: -212,88,177987,1,8,0:0:0:0: -272,228,178152,1,0,0:0:0:0: -272,228,178235,1,0,0:0:0:0: -272,228,178317,1,0,0:0:0:0: -292,124,178482,1,0,1:0:0:0: -180,180,178647,1,8,0:0:0:0: -200,284,178812,1,0,0:0:0:0: -292,124,178976,5,0,1:0:0:0: -288,92,179059,1,0,0:0:0:0: -280,60,179141,2,0,P|248:24|208:14,1,90,0|8,0:0|0:0,0:0:0:0: -22,65,179471,2,0,P|67:71|112:68,1,90,0|0,1:0|0:0,0:0:0:0: -212,88,179801,1,0,1:0:0:0: -22,65,179965,1,8,0:0:0:0: -180,180,180130,5,0,0:0:0:0: -180,180,180213,1,0,0:0:0:0: -180,180,180295,2,0,P|184:224|172:272,1,90,0|0,1:0|0:0,0:0:0:0: -76,216,180625,2,0,P|72:172|84:124,1,90,8|0,0:0|0:0,0:0:0:0: -380,240,180954,2,0,P|384:284|372:332,1,90,0|0,0:0|1:0,0:0:0:0: -276,276,181284,2,0,P|272:232|284:184,1,90,8|0,0:0|0:0,0:0:0:0: -374,129,181614,5,0,1:0:0:0: -300,352,181779,2,0,L|204:348,2,90,0|8|0,0:0|0:0|1:0,0:0:0:0: -448,180,182273,1,2,0:0:0:0: -448,180,182438,1,2,1:2:0:0: -276,276,182603,1,10,0:0:0:0: -276,276,182768,1,2,0:0:0:0: -96,200,182932,6,0,L|88:108,1,90,0|0,1:0|0:0,0:0:0:0: -96,200,183262,1,8,0:0:0:0: -12,68,183427,2,0,P|72:24|164:68,1,180,0|0,0:0|1:0,0:0:0:0: -140,272,183921,2,0,P|92:284|52:271,1,90,8|0,0:0|0:0,0:0:0:0: -176,156,184251,5,0,1:0:0:0: -208,152,184334,1,0,1:0:0:0: -240,148,184416,1,0,1:0:0:0: -308,64,184581,1,8,0:0:0:0: -296,240,184746,1,0,1:0:0:0: -312,268,184828,1,0,1:0:0:0: -336,284,184910,1,0,1:0:0:0: -368,292,184993,1,0,1:0:0:0: -400,288,185075,1,0,1:0:0:0: -464,184,185240,1,8,0:0:0:0: -468,152,185323,1,0,0:0:0:0: -472,120,185405,2,0,L|464:76,1,45,0|0,1:0|1:0,0:0:0:0: -388,96,185570,6,0,P|360:132|316:148,1,90,4|0,1:2|0:0,0:0:0:0: -224,46,185899,2,0,P|268:43|308:63,1,90,8|0,0:0|0:0,0:0:0:0: -296,240,186229,1,0,0:0:0:0: -308,64,186394,1,0,1:0:0:0: -296,240,186559,2,0,L|312:332,1,90,8|0,0:0|0:0,0:0:0:0: -464,184,186888,6,0,P|420:180|372:188,1,90,0|0,1:0|0:0,0:0:0:0: -296,240,187218,1,8,0:0:0:0: -136,292,187383,2,0,P|94:277|54:249,1,90,0|0,1:0|0:0,0:0:0:0: -21,159,187713,1,0,1:0:0:0: -104,8,187877,2,0,L|124:96,1,90,10|0,0:0|0:0,0:0:0:0: -124,96,188207,6,0,P|152:132|196:148,1,90,0|0,1:0|0:0,0:0:0:0: -287,46,188537,2,0,P|243:43|204:63,1,90,8|0,0:0|0:0,0:0:0:0: -216,240,188866,1,2,0:0:0:0: -204,64,189031,1,0,1:0:0:0: -216,240,189196,2,0,L|200:332,1,90,8|0,0:0|0:0,0:0:0:0: -40,240,189526,5,2,1:2:0:0: -128,192,189691,1,0,0:0:0:0: -216,240,189855,1,8,0:0:0:0: -304,192,190020,1,0,1:0:0:0: -392,240,190185,2,0,L|400:332,1,90,2|0,0:0|1:0,0:0:0:0: -464,168,190515,2,0,L|456:76,1,90,8|0,0:0|0:0,0:0:0:0: -392,240,190844,6,0,P|364:272|312:292,1,90,2|0,1:2|0:0,0:0:0:0: -220,140,191174,2,0,P|248:108|296:92,1,90,8|0,0:0|0:0,0:0:0:0: -324,96,191421,1,0,0:0:0:0: -356,104,191504,2,0,L|340:16,1,90,0|0,0:0|1:0,0:0:0:0: -256,276,191834,2,0,L|272:364,1,90,8|0,0:0|0:0,0:0:0:0: -392,240,192163,5,0,1:0:0:0: -356,104,192328,1,0,0:0:0:0: -220,140,192493,1,8,0:0:0:0: -256,276,192658,1,0,1:0:0:0: -305,191,192823,1,0,0:0:0:0: -212,56,192987,1,0,1:0:0:0: -200,220,193152,1,10,0:0:0:0: -200,220,193482,6,0,P|156:228|108:220,1,90,0|0,1:0|0:0,0:0:0:0: -88,116,193812,1,8,0:0:0:0: -16,192,193976,1,0,0:0:0:0: -16,192,194059,1,0,0:0:0:0: -16,192,194141,2,0,L|28:288,1,90,2|0,0:0|1:0,0:0:0:0: -188,309,194471,2,0,L|200:220,1,90,8|0,0:0|0:0,0:0:0:0: -216,112,194801,5,2,1:2:0:0: -216,112,194883,1,0,1:0:0:0: -216,112,194965,1,0,1:0:0:0: -361,25,195130,1,8,0:0:0:0: -294,180,195295,1,0,1:0:0:0: -294,180,195377,1,0,1:0:0:0: -294,180,195460,1,2,0:0:0:0: -256,16,195625,1,0,1:0:0:0: -384,127,195790,1,10,1:2:0:0: -416,132,195872,1,0,1:0:0:0: -448,140,195954,2,0,L|452:84,1,45,0|0,1:0|1:0,0:0:0:0: -416,216,196119,6,0,P|412:264|432:312,1,90,4|0,1:2|0:0,0:0:0:0: -304,268,196449,2,0,P|308:220|288:172,1,90,8|0,0:0|0:0,0:0:0:0: -216,112,196779,2,0,L|120:104,1,90,0|0,0:0|1:0,0:0:0:0: -52,248,197108,2,0,L|141:255,1,90,8|0,0:0|0:0,0:0:0:0: -304,268,197438,5,0,1:0:0:0: -416,216,197603,1,0,0:0:0:0: -408,340,197768,1,8,0:0:0:0: -332,180,197932,1,0,1:0:0:0: -332,180,198015,1,0,0:0:0:0: -332,180,198097,2,0,P|360:140|400:120,1,90,0|0,0:0|1:0,0:0:0:0: -484,284,198427,1,10,0:0:0:0: -304,268,198592,1,2,0:0:0:0: -416,216,198757,6,0,P|428:172|420:124,1,90,2|0,1:2|0:0,0:0:0:0: -344,52,199086,1,8,0:0:0:0: -332,180,199251,1,0,0:0:0:0: -164,236,199416,2,0,P|152:192|160:144,1,90,0|0,0:0|1:0,0:0:0:0: -236,72,199746,1,8,0:0:0:0: -248,200,199910,1,0,0:0:0:0: -156,328,200075,6,0,L|56:320,1,90,2|0,1:2|0:0,0:0:0:0: -164,236,200405,1,8,0:0:0:0: -256,292,200570,2,0,P|300:296|344:284,1,90,0|0,1:0|0:0,0:0:0:0: -432,220,200899,2,0,L|460:308,2,90,0|8|0,1:0|0:0|0:0,0:0:0:0: -392,120,201394,5,4,1:2:0:0: -396,32,201559,1,0,1:0:0:0: -316,72,201724,1,0,1:0:0:0: -256,6,201888,1,0,1:0:0:0: -228,91,202053,1,0,1:0:0:0: -139,87,202218,1,0,1:0:0:0: -179,166,202383,1,0,1:0:0:0: -113,226,202548,1,0,1:0:0:0: -197,253,202713,5,4,1:2:0:0: -193,342,202877,1,0,1:0:0:0: -272,302,203042,1,0,1:0:0:0: -332,367,203207,1,0,1:0:0:0: -359,283,203372,1,2,1:2:0:0: -448,287,203537,1,2,1:2:0:0: -407,208,203702,1,2,1:2:0:0: -472,147,203866,1,2,1:2:0:0: -387,121,204031,5,4,1:2:0:0: -360,100,204114,1,0,1:0:0:0: -344,72,204196,1,0,1:0:0:0: -336,40,204279,1,0,1:0:0:0: -340,8,204361,1,0,1:0:0:0: -316,28,204443,1,0,1:0:0:0: -284,32,204526,1,0,1:0:0:0: -252,28,204608,1,0,1:0:0:0: -228,8,204691,2,0,L|184:20,7,45,4|0|0|0|0|0|0|0,1:2|1:0|1:0|1:0|1:0|1:0|1:0|1:0,0:0:0:0: -112,56,205350,5,4,1:2:0:0: -100,84,205432,1,0,1:0:0:0: -96,116,205515,1,0,1:0:0:0: -100,148,205597,1,0,1:0:0:0: -112,176,205680,1,0,1:0:0:0: -124,204,205762,1,0,1:0:0:0: -128,236,205844,1,0,1:0:0:0: -124,268,205927,1,0,1:0:0:0: -112,296,206009,2,0,L|71:313,3,45,2|0|2|0,1:2|0:0|0:0|0:0,0:0:0:0: -192,312,206339,2,0,L|175:353,3,45,2|0|2|0,1:2|0:0|0:0|0:0,0:0:0:0: -256,264,206669,5,4,1:2:0:0: -256,192,206751,12,0,209306,0:0:0:0: diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs new file mode 100644 index 0000000000..4487f74364 --- /dev/null +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -0,0 +1,111 @@ +// 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.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit.Layers.Selection; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Screens.Compose.RadioButtons; + +namespace osu.Game.Rulesets.Edit +{ + public abstract class HitObjectComposer : CompositeDrawable + { + private readonly Ruleset ruleset; + + protected ICompositionTool CurrentTool { get; private set; } + + protected HitObjectComposer(Ruleset ruleset) + { + this.ruleset = ruleset; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame) + { + RulesetContainer rulesetContainer; + try + { + rulesetContainer = CreateRulesetContainer(ruleset, osuGame.Beatmap.Value); + } + catch (Exception e) + { + Logger.Log($"Could not load this beatmap sucessfully ({e})!", LoggingTarget.Runtime, LogLevel.Error); + return; + } + + RadioButtonCollection toolboxCollection; + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + Name = "Sidebar", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 10 }, + Children = new Drawable[] + { + new ToolboxGroup { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + rulesetContainer, + new SelectionLayer(rulesetContainer.Playfield) + } + } + }, + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 200), + } + }; + + rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock()); + + toolboxCollection.Items = + new[] { new RadioButton("Select", () => setCompositionTool(null)) } + .Concat( + CompositionTools.Select(t => new RadioButton(t.Name, () => setCompositionTool(t))) + ) + .ToList(); + + toolboxCollection.Items[0].Select(); + } + + private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool; + + protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true); + + protected abstract IReadOnlyList CompositionTools { get; } + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/Handle.cs b/osu.Game/Rulesets/Edit/Layers/Selection/Handle.cs new file mode 100644 index 0000000000..2982a68b3b --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/Handle.cs @@ -0,0 +1,105 @@ +// 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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Game.Graphics; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + /// + /// Represents a marker visible on the border of a which exposes + /// properties that are used to resize a . + /// + public class Handle : CompositeDrawable + { + private const float marker_size = 10; + + /// + /// Invoked when this requires the current drag rectangle. + /// + public Func GetDragRectangle; + + /// + /// Invoked when this wants to update the drag rectangle. + /// + public Action UpdateDragRectangle; + + /// + /// Invoked when this has finished updates to the drag rectangle. + /// + public Action FinishDrag; + + private Color4 normalColour; + private Color4 hoverColour; + + public Handle() + { + Size = new Vector2(marker_size); + + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = normalColour = colours.Yellow; + hoverColour = colours.YellowDarker; + } + + protected override bool OnDragStart(InputState state) => true; + + protected override bool OnDrag(InputState state) + { + var currentRectangle = GetDragRectangle(); + + float left = currentRectangle.Left; + float right = currentRectangle.Right; + float top = currentRectangle.Top; + float bottom = currentRectangle.Bottom; + + // Apply modifications to the capture rectangle + if ((Anchor & Anchor.y0) > 0) + top += state.Mouse.Delta.Y; + else if ((Anchor & Anchor.y2) > 0) + bottom += state.Mouse.Delta.Y; + + if ((Anchor & Anchor.x0) > 0) + left += state.Mouse.Delta.X; + else if ((Anchor & Anchor.x2) > 0) + right += state.Mouse.Delta.X; + + UpdateDragRectangle(RectangleF.FromLTRB(left, top, right, bottom)); + return true; + } + + protected override bool OnDragEnd(InputState state) + { + FinishDrag(); + return true; + } + + protected override bool OnHover(InputState state) + { + this.FadeColour(hoverColour, 100); + return true; + } + + protected override void OnHoverLost(InputState state) + { + this.FadeColour(normalColour, 100); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/HandleContainer.cs b/osu.Game/Rulesets/Edit/Layers/Selection/HandleContainer.cs new file mode 100644 index 0000000000..22d993e7cd --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/HandleContainer.cs @@ -0,0 +1,92 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + /// + /// A that has s around its border. + /// + public class HandleContainer : CompositeDrawable + { + /// + /// Invoked when a requires the current drag rectangle. + /// + public Func GetDragRectangle; + + /// + /// Invoked when a wants to update the drag rectangle. + /// + public Action UpdateDragRectangle; + + /// + /// Invoked when a has finished updates to the drag rectangle. + /// + public Action FinishDrag; + + public HandleContainer() + { + InternalChildren = new Drawable[] + { + new Handle + { + Anchor = Anchor.TopLeft, + Origin = Anchor.Centre + }, + new Handle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre + }, + new Handle + { + Anchor = Anchor.TopRight, + Origin = Anchor.Centre + }, + new Handle + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre + }, + new Handle + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.Centre + }, + new Handle + { + Anchor = Anchor.CentreRight, + Origin = Anchor.Centre + }, + new Handle + { + Anchor = Anchor.BottomRight, + Origin = Anchor.Centre + }, + new Handle + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre + }, + new OriginHandle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }; + + InternalChildren.OfType().ForEach(m => + { + m.GetDragRectangle = () => GetDragRectangle(); + m.UpdateDragRectangle = r => UpdateDragRectangle(r); + m.FinishDrag = () => FinishDrag(); + }); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectSelectionBox.cs b/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectSelectionBox.cs new file mode 100644 index 0000000000..6f73d6b916 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectSelectionBox.cs @@ -0,0 +1,178 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Configuration; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + /// + /// A box that represents a drag selection. + /// + public class HitObjectSelectionBox : CompositeDrawable + { + public readonly Bindable Selection = new Bindable(); + + /// + /// The s that can be selected through a drag-selection. + /// + public IEnumerable CapturableObjects; + + private readonly Container borderMask; + private readonly Drawable background; + private readonly HandleContainer handles; + + private Color4 captureFinishedColour; + + private readonly Vector2 startPos; + + /// + /// Creates a new . + /// + /// The point at which the drag was initiated, in the parent's coordinates. + public HitObjectSelectionBox(Vector2 startPos) + { + this.startPos = startPos; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(-1), + Child = borderMask = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2, + MaskingSmoothness = 1, + Child = background = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f, + AlwaysPresent = true + }, + } + }, + handles = new HandleContainer + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + GetDragRectangle = () => dragRectangle, + UpdateDragRectangle = updateDragRectangle, + FinishDrag = FinishCapture + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + captureFinishedColour = colours.Yellow; + } + + /// + /// The secondary corner of the drag selection box. A rectangle will be fit between the starting position and this value. + /// + public Vector2 DragEndPosition { set => updateDragRectangle(RectangleF.FromLTRB(startPos.X, startPos.Y, value.X, value.Y)); } + + private RectangleF dragRectangle; + private void updateDragRectangle(RectangleF rectangle) + { + dragRectangle = rectangle; + + Position = new Vector2( + Math.Min(rectangle.Left, rectangle.Right), + Math.Min(rectangle.Top, rectangle.Bottom)); + + Size = new Vector2( + Math.Max(rectangle.Left, rectangle.Right) - Position.X, + Math.Max(rectangle.Top, rectangle.Bottom) - Position.Y); + } + + private readonly List capturedHitObjects = new List(); + + /// + /// Processes hitobjects to determine which ones are captured by the drag selection. + /// Captured hitobjects will be enclosed by the drag selection upon . + /// + public void BeginCapture() + { + capturedHitObjects.Clear(); + + foreach (var obj in CapturableObjects) + { + if (!obj.IsAlive || !obj.IsPresent) + continue; + + if (ScreenSpaceDrawQuad.Contains(obj.SelectionPoint)) + capturedHitObjects.Add(obj); + } + } + + /// + /// Encloses hitobjects captured through in the drag selection box. + /// + public void FinishCapture() + { + if (capturedHitObjects.Count == 0) + { + Hide(); + return; + } + + // Move the rectangle to cover the hitobjects + var topLeft = new Vector2(float.MaxValue, float.MaxValue); + var bottomRight = new Vector2(float.MinValue, float.MinValue); + + foreach (var obj in capturedHitObjects) + { + topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(obj.SelectionQuad.TopLeft)); + bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.SelectionQuad.BottomRight)); + } + + topLeft -= new Vector2(5); + bottomRight += new Vector2(5); + + this.MoveTo(topLeft, 200, Easing.OutQuint) + .ResizeTo(bottomRight - topLeft, 200, Easing.OutQuint); + + dragRectangle = RectangleF.FromLTRB(topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y); + + borderMask.BorderThickness = 3; + borderMask.FadeColour(captureFinishedColour, 200); + + // Transform into markers to let the user modify the drag selection further. + background.Delay(50).FadeOut(200); + handles.FadeIn(200); + + Selection.Value = new SelectionInfo + { + Objects = capturedHitObjects, + SelectionQuad = Parent.ToScreenSpace(dragRectangle) + }; + } + + private bool isActive = true; + public override bool HandleInput => isActive; + + public override void Hide() + { + isActive = false; + this.FadeOut(400, Easing.OutQuint).Expire(); + + Selection.Value = null; + } + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/OriginHandle.cs b/osu.Game/Rulesets/Edit/Layers/Selection/OriginHandle.cs new file mode 100644 index 0000000000..8326ebeeac --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/OriginHandle.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using OpenTK; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + /// + /// Represents the origin of a . + /// + public class OriginHandle : CompositeDrawable + { + private const float marker_size = 10; + private const float line_width = 2; + + public OriginHandle() + { + Size = new Vector2(marker_size); + + InternalChildren = new[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = line_width + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = line_width + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Yellow; + } + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionInfo.cs b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionInfo.cs new file mode 100644 index 0000000000..aec16bd46d --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionInfo.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.Collections.Generic; +using osu.Framework.Graphics.Primitives; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + public class SelectionInfo + { + /// + /// The objects that are captured by the selection. + /// + public IEnumerable Objects; + + /// + /// The screen space quad of the selection box surrounding . + /// + public Quad SelectionQuad; + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs new file mode 100644 index 0000000000..98bcfd0ec8 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Edit.Layers.Selection +{ + public class SelectionLayer : CompositeDrawable + { + public readonly Bindable Selection = new Bindable(); + + private readonly Playfield playfield; + + public SelectionLayer(Playfield playfield) + { + this.playfield = playfield; + + RelativeSizeAxes = Axes.Both; + } + + private HitObjectSelectionBox selectionBoxBox; + + protected override bool OnDragStart(InputState state) + { + // Hide the previous drag box - we won't be working with it any longer + selectionBoxBox?.Hide(); + + AddInternal(selectionBoxBox = new HitObjectSelectionBox(ToLocalSpace(state.Mouse.NativeState.Position)) + { + CapturableObjects = playfield.HitObjects.Objects, + }); + + Selection.BindTo(selectionBoxBox.Selection); + + return true; + } + + protected override bool OnDrag(InputState state) + { + selectionBoxBox.DragEndPosition = ToLocalSpace(state.Mouse.NativeState.Position); + selectionBoxBox.BeginCapture(); + return true; + } + + protected override bool OnDragEnd(InputState state) + { + selectionBoxBox.FinishCapture(); + return true; + } + + protected override bool OnClick(InputState state) + { + selectionBoxBox?.Hide(); + return true; + } + } +} diff --git a/osu.Game/Rulesets/Edit/ToolboxGroup.cs b/osu.Game/Rulesets/Edit/ToolboxGroup.cs new file mode 100644 index 0000000000..70e4d3a0c5 --- /dev/null +++ b/osu.Game/Rulesets/Edit/ToolboxGroup.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.Framework.Graphics; +using osu.Game.Screens.Play.ReplaySettings; + +namespace osu.Game.Rulesets.Edit +{ + public class ToolboxGroup : ReplayGroup + { + protected override string Title => "toolbox"; + + public ToolboxGroup() + { + RelativeSizeAxes = Axes.X; + Width = 1; + } + } +} diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs new file mode 100644 index 0000000000..dd182dcbdb --- /dev/null +++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit.Tools +{ + public class HitObjectCompositionTool : ICompositionTool + where T : HitObject + { + public string Name => typeof(T).Name; + } +} diff --git a/osu.Game/Rulesets/Edit/Tools/ICompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/ICompositionTool.cs new file mode 100644 index 0000000000..eba873f0cf --- /dev/null +++ b/osu.Game/Rulesets/Edit/Tools/ICompositionTool.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Edit.Tools +{ + public interface ICompositionTool + { + string Name { get; } + } +} diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 12edfd802a..5ab4b7636b 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Judgements break; } - Expire(); + Expire(true); } } } diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 0ae33272a7..d804111a7f 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -17,13 +17,31 @@ namespace osu.Game.Rulesets.Judgements /// public virtual HitResult MaxResult => HitResult.Perfect; + /// + /// The combo prior to this judgement occurring. + /// + public int ComboAtJudgement; + + /// + /// The highest combo achieved prior to this judgement occurring. + /// + public int HighestComboAtJudgement; + + /// + /// Whether a successful hit occurred. + /// public bool IsHit => Result > HitResult.Miss; + /// + /// Whether this judgement is the final judgement for the hit object. + /// + public bool Final = true; + /// /// The offset from a perfect hit at which this judgement occurred. /// Populated when added via . /// - public double TimeOffset { get; internal set; } + public double TimeOffset { get; set; } /// /// Whether the should affect the combo portion of the score. diff --git a/osu.Game/Rulesets/Mods/IApplicableMod.cs b/osu.Game/Rulesets/Mods/IApplicableMod.cs deleted file mode 100644 index 1957952720..0000000000 --- a/osu.Game/Rulesets/Mods/IApplicableMod.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.Game.Rulesets.Objects; -using osu.Game.Rulesets.UI; - -namespace osu.Game.Rulesets.Mods -{ - /// - /// An interface for mods that are applied to a RulesetContainer. - /// - /// The type of HitObject the RulesetContainer contains. - public interface IApplicableMod - where TObject : HitObject - { - /// - /// Applies the mod to a RulesetContainer. - /// - /// The RulesetContainer to apply the mod to. - void ApplyToRulesetContainer(RulesetContainer rulesetContainer); - } -} diff --git a/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs new file mode 100644 index 0000000000..7f39def343 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToHitObject.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 osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// An interface for s that can be applied to s. + /// + public interface IApplicableToHitObject + where TObject : HitObject + { + /// + /// Applies this to a . + /// + /// The to apply to. + void ApplyToHitObject(TObject hitObject); + } +} diff --git a/osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.cs b/osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.cs new file mode 100644 index 0000000000..9b23dd58f9 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.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.Rulesets.Objects; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// An interface for s that can be applied to s. + /// + public interface IApplicableToRulesetContainer + where TObject : HitObject + { + /// + /// Applies this to a . + /// + /// The to apply to. + void ApplyToRulesetContainer(RulesetContainer rulesetContainer); + } +} diff --git a/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs new file mode 100644 index 0000000000..db9b713c59 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.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 osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// An interface for mods that make general adjustments to score processor. + /// + public interface IApplicableToScoreProcessor + { + void ApplyToScoreProcessor(ScoreProcessor scoreProcessor); + } +} diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index ece0deba84..d94d4ba0db 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { - public abstract class ModAutoplay : ModAutoplay, IApplicableMod + public abstract class ModAutoplay : ModAutoplay, IApplicableToRulesetContainer where T : HitObject { protected abstract Score CreateReplayScore(Beatmap beatmap); @@ -30,4 +30,4 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 0; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 082370ea5d..59539d2b2c 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Scoring; + namespace osu.Game.Rulesets.Mods { public abstract class ModPerfect : ModSuddenDeath @@ -8,5 +10,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Perfect"; public override string ShortenedName => "PF"; public override string Description => "SS or quit."; + + protected override bool FailCondition(ScoreProcessor scoreProcessor) => scoreProcessor.Accuracy.Value != 1; } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 999cb40f89..bc42c69cbe 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -3,10 +3,11 @@ using System; using osu.Game.Graphics; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModSuddenDeath : Mod + public abstract class ModSuddenDeath : Mod, IApplicableToScoreProcessor { public override string Name => "Sudden Death"; public override string ShortenedName => "SD"; @@ -16,5 +17,12 @@ namespace osu.Game.Rulesets.Mods public override double ScoreMultiplier => 1; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; + + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + { + scoreProcessor.FailConditions += FailCondition; + } + + protected virtual bool FailCondition(ScoreProcessor scoreProcessor) => scoreProcessor.Combo.Value == 0; } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7a26a53c2a..d960ab6b48 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -13,6 +13,9 @@ using OpenTK.Graphics; using osu.Game.Audio; using System.Linq; using osu.Game.Graphics; +using osu.Framework.Configuration; +using OpenTK; +using osu.Framework.Graphics.Primitives; namespace osu.Game.Rulesets.Objects.Drawables { @@ -25,16 +28,35 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public virtual Color4 AccentColour { get; set; } = Color4.Gray; + /// + /// Whether a visible judgement should be displayed when this representation is hit. + /// + public virtual bool DisplayJudgement => true; + + public override bool RemoveCompletedTransforms => false; + public override bool RemoveWhenNotAlive => false; + protected DrawableHitObject(HitObject hitObject) { HitObject = hitObject; } + + /// + /// The screen-space point that causes this to be selected in the Editor. + /// + public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre; + + /// + /// The screen-space quad that outlines this for selections in the Editor. + /// + public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; } public abstract class DrawableHitObject : DrawableHitObject where TObject : HitObject { public event Action OnJudgement; + public event Action OnJudgementRemoved; public new readonly TObject HitObject; @@ -51,31 +73,54 @@ namespace osu.Game.Rulesets.Objects.Drawables protected List Samples = new List(); + public readonly Bindable State = new Bindable(); + protected DrawableHitObject(TObject hitObject) : base(hitObject) { HitObject = hitObject; } - private ArmedState state; - public ArmedState State + [BackgroundDependencyLoader] + private void load(AudioManager audio) { - get { return state; } - - set + if (HitObject.Samples != null) { - if (state == value) - return; - state = value; + if (HitObject.SampleControlPoint == null) + throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)} must always have an attached {nameof(HitObject.SampleControlPoint)}."); - if (!IsLoaded) - return; + foreach (SampleInfo s in HitObject.Samples) + { + SampleInfo localSampleInfo = new SampleInfo + { + Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank, + Name = s.Name, + Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume + }; + SampleChannel channel = localSampleInfo.GetChannel(audio.Sample); + + if (channel == null) + continue; + + Samples.Add(channel); + } + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + State.ValueChanged += state => + { UpdateState(state); if (State == ArmedState.Hit) PlaySamples(); - } + }; + + State.TriggerChange(); } protected void PlaySamples() @@ -83,21 +128,13 @@ namespace osu.Game.Rulesets.Objects.Drawables Samples.ForEach(s => s?.Play()); } - protected override void LoadComplete() - { - base.LoadComplete(); - - //force application of the state that was set before we loaded. - UpdateState(State); - } - - private bool hasJudgementResult; private bool judgementOccurred; + private bool judgementFinalized => judgements.LastOrDefault()?.Final == true; /// /// Whether this and all of its nested s have been judged. /// - public virtual bool AllJudged => (!ProvidesJudgement || hasJudgementResult) && (NestedHitObjects?.All(h => h.AllJudged) ?? true); + public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && (NestedHitObjects?.All(h => h.AllJudged) ?? true); /// /// Notifies that a new judgement has occurred for this . @@ -105,7 +142,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// The . protected void AddJudgement(Judgement judgement) { - hasJudgementResult = judgement.Result >= HitResult.Miss; judgementOccurred = true; // Ensure that the judgement is given a valid time offset, because this may not get set by the caller @@ -119,10 +155,10 @@ namespace osu.Game.Rulesets.Objects.Drawables case HitResult.None: break; case HitResult.Miss: - State = ArmedState.Miss; + State.Value = ArmedState.Miss; break; default: - State = ArmedState.Hit; + State.Value = ArmedState.Hit; break; } @@ -147,7 +183,7 @@ namespace osu.Game.Rulesets.Objects.Drawables judgementOccurred |= d.UpdateJudgement(userTriggered); } - if (!ProvidesJudgement || hasJudgementResult || judgementOccurred) + if (!ProvidesJudgement || judgementFinalized || judgementOccurred) return judgementOccurred; var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; @@ -165,6 +201,25 @@ namespace osu.Game.Rulesets.Objects.Drawables /// implies that this check occurred after the end time of . protected virtual void CheckForJudgements(bool userTriggered, double timeOffset) { } + protected override void Update() + { + base.Update(); + + var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; + + while (judgements.Count > 0) + { + var lastJudgement = judgements[judgements.Count - 1]; + if (lastJudgement.TimeOffset + endTime <= Time.Current) + break; + + judgements.RemoveAt(judgements.Count - 1); + State.Value = ArmedState.Idle; + + OnJudgementRemoved?.Invoke(this, lastJudgement); + } + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -172,21 +227,6 @@ namespace osu.Game.Rulesets.Objects.Drawables UpdateJudgement(false); } - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - foreach (SampleInfo sample in HitObject.Samples) - { - SampleChannel channel = audio.Sample.Get($@"Gameplay/{sample.Bank}-{sample.Name}"); - - if (channel == null) - continue; - - channel.Volume.Value = sample.Volume; - Samples.Add(channel); - } - } - private List> nestedHitObjects; protected IEnumerable> NestedHitObjects => nestedHitObjects; @@ -196,6 +236,7 @@ namespace osu.Game.Rulesets.Objects.Drawables nestedHitObjects = new List>(); h.OnJudgement += (d, j) => OnJudgement?.Invoke(d, j); + h.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(d, j); nestedHitObjects.Add(h); } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index c69979d4cf..e950516bf4 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -1,6 +1,10 @@ // 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.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Lists; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -28,41 +32,49 @@ namespace osu.Game.Rulesets.Objects /// and can be treated as the default samples for the hit object. /// /// - public SampleInfoList Samples = new SampleInfoList(); + public SampleInfoList Samples; + + [JsonIgnore] + public SampleControlPoint SampleControlPoint; /// /// Whether this is in Kiai time. /// + [JsonIgnore] public bool Kiai { get; private set; } + private readonly SortedList nestedHitObjects = new SortedList((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); + + [JsonIgnore] + public IReadOnlyList NestedHitObjects => nestedHitObjects; + /// /// Applies default values to this HitObject. /// /// The control points. /// The difficulty settings to use. - public virtual void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { - SoundControlPoint soundPoint = controlPointInfo.SoundPointAt(StartTime); + ApplyDefaultsToSelf(controlPointInfo, difficulty); + + nestedHitObjects.Clear(); + CreateNestedHitObjects(); + nestedHitObjects.ForEach(h => h.ApplyDefaults(controlPointInfo, difficulty)); + } + + protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + SampleControlPoint samplePoint = controlPointInfo.SamplePointAt(StartTime); EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime); - Kiai |= effectPoint.KiaiMode; - - // Initialize first sample - Samples.ForEach(s => initializeSampleInfo(s, soundPoint)); - - // Initialize any repeat samples - var repeatData = this as IHasRepeats; - repeatData?.RepeatSamples?.ForEach(r => r.ForEach(s => initializeSampleInfo(s, soundPoint))); + Kiai = effectPoint.KiaiMode; + SampleControlPoint = samplePoint; } - private void initializeSampleInfo(SampleInfo sample, SoundControlPoint soundPoint) + protected virtual void CreateNestedHitObjects() { - if (sample.Volume == 0) - sample.Volume = soundPoint?.SampleVolume ?? 0; - - // If the bank is not assigned a name, assign it from the control point - if (string.IsNullOrEmpty(sample.Bank)) - sample.Bank = soundPoint?.SampleBank ?? @"normal"; } + + protected void AddNested(HitObject hitObject) => nestedHitObjects.Add(hitObject); } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index c7f7802191..667f921e04 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// A HitObjectParser to parse legacy osu!catch Beatmaps. /// - internal class ConvertHitObjectParser : Legacy.ConvertHitObjectParser + public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { protected override HitObject CreateHit(Vector2 position, bool newCombo) { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index b5e3f837fc..0d7d617405 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// A HitObjectParser to parse legacy Beatmaps. /// - internal abstract class ConvertHitObjectParser : HitObjectParser + public abstract class ConvertHitObjectParser : HitObjectParser { public override HitObject Parse(string text) { @@ -177,8 +177,8 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = str.Split(':'); - var bank = (OsuLegacyDecoder.LegacySampleBank)Convert.ToInt32(split[0]); - var addbank = (OsuLegacyDecoder.LegacySampleBank)Convert.ToInt32(split[1]); + var bank = (LegacyDecoder.LegacySampleBank)Convert.ToInt32(split[0]); + var addbank = (LegacyDecoder.LegacySampleBank)Convert.ToInt32(split[1]); // Let's not implement this for now, because this doesn't fit nicely into the bank structure //string sampleFile = split2.Length > 4 ? split2[4] : string.Empty; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index 479cacc52b..698b74cc28 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -46,9 +46,9 @@ namespace osu.Game.Rulesets.Objects.Legacy throw new NotImplementedException(); } - public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { - base.ApplyDefaults(controlPointInfo, difficulty); + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs index 7141876b8b..86dd40b06e 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// /// A HitObjectParser to parse legacy osu!mania Beatmaps. /// - internal class ConvertHitObjectParser : Legacy.ConvertHitObjectParser + public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { protected override HitObject CreateHit(Vector2 position, bool newCombo) { diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index 00fe171f0f..24c205db13 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// A HitObjectParser to parse legacy osu! Beatmaps. /// - internal class ConvertHitObjectParser : Legacy.ConvertHitObjectParser + public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { protected override HitObject CreateHit(Vector2 position, bool newCombo) { diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs index 5929c5a907..0554cfd97d 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko /// /// A HitObjectParser to parse legacy osu!taiko Beatmaps. /// - internal class ConvertHitObjectParser : Legacy.ConvertHitObjectParser + public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { protected override HitObject CreateHit(Vector2 position, bool newCombo) { diff --git a/osu.Game/Rulesets/Objects/SliderCurve.cs b/osu.Game/Rulesets/Objects/SliderCurve.cs index 8bf85e498c..363c330d3d 100644 --- a/osu.Game/Rulesets/Objects/SliderCurve.cs +++ b/osu.Game/Rulesets/Objects/SliderCurve.cs @@ -200,4 +200,4 @@ namespace osu.Game.Rulesets.Objects return interpolateVertices(indexOfDistance(d), d) + Offset; } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index f0d68c0467..06f7259f78 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -104,7 +104,13 @@ namespace osu.Game.Rulesets.Replays { //if we changed frames, we want to execute once *exactly* on the frame's time. if (currentDirection == time.CompareTo(NextFrame.Time) && advanceFrame()) + { + // If going backwards, we need to execute once _before_ the frame time to reverse any judgements + // that would occur as a result of this frame in forward playback + if (currentDirection == -1) + return currentTime = CurrentFrame.Time - 1; return currentTime = CurrentFrame.Time; + } //if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null. if (inImportantSection) diff --git a/osu.Game/Rulesets/Replays/ReplayFrame.cs b/osu.Game/Rulesets/Replays/ReplayFrame.cs index b0f62e5271..02c969f648 100644 --- a/osu.Game/Rulesets/Replays/ReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/ReplayFrame.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Replays { public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); - public bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight); + public virtual bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight); public float? MouseX; public float? MouseY; @@ -68,4 +68,4 @@ namespace osu.Game.Rulesets.Replays return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}"; } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 587c2fc659..64a2157069 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -9,7 +9,9 @@ using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets @@ -21,13 +23,12 @@ namespace osu.Game.Rulesets public virtual IEnumerable GetBeatmapStatistics(WorkingBeatmap beatmap) => new BeatmapStatistic[] { }; public IEnumerable GetAllMods() => Enum.GetValues(typeof(ModType)).Cast() - // Get all mod types as an IEnumerable - .SelectMany(GetModsFor) - // Confine all mods of each mod type into a single IEnumerable - .Where(mod => mod != null) - // Filter out all null mods - .SelectMany(mod => (mod as MultiMod)?.Mods ?? new[] { mod }); - // Resolve MultiMods as their .Mods property + // Confine all mods of each mod type into a single IEnumerable + .SelectMany(GetModsFor) + // Filter out all null mods + .Where(mod => mod != null) + // Resolve MultiMods as their .Mods property + .SelectMany(mod => (mod as MultiMod)?.Mods ?? new[] { mod }); public abstract IEnumerable GetModsFor(ModType type); @@ -47,7 +48,11 @@ namespace osu.Game.Rulesets /// public abstract RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset); - public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap); + public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null); + + public virtual PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => null; + + public virtual HitObjectComposer CreateHitObjectComposer() => null; public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_question_circle }; @@ -60,6 +65,11 @@ namespace osu.Game.Rulesets /// public virtual int LegacyID => -1; + /// + /// A unique short name to reference this ruleset in online requests. + /// + public abstract string ShortName { get; } + /// /// A list of available variant ids. /// diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 740369b1b6..c5324d92a9 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -2,22 +2,24 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using SQLite.Net.Attributes; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; namespace osu.Game.Rulesets { public class RulesetInfo : IEquatable { - [PrimaryKey, AutoIncrement] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [JsonIgnore] public int? ID { get; set; } - [Indexed(Unique = true)] public string Name { get; set; } - [Indexed(Unique = true)] + public string ShortName { get; set; } + public string InstantiationInfo { get; set; } - [Indexed] + [JsonIgnore] public bool Available { get; set; } public virtual Ruleset CreateInstance() => (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo), this); diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 5eef4a8470..5cdf46ee46 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Reflection; using osu.Game.Database; -using SQLite.Net; namespace osu.Game.Rulesets { @@ -18,12 +17,6 @@ namespace osu.Game.Rulesets { private static readonly Dictionary loaded_assemblies = new Dictionary(); - public IEnumerable AllRulesets => Query().Where(r => r.Available); - - public RulesetStore(SQLiteConnection connection) : base(connection) - { - } - static RulesetStore() { AppDomain.CurrentDomain.AssemblyResolve += currentDomain_AssemblyResolve; @@ -32,60 +25,80 @@ namespace osu.Game.Rulesets loadRulesetFromFile(file); } + public RulesetStore(Func factory) + : base(factory) + { + AddMissingRulesets(); + } + + /// + /// Retrieve a ruleset using a known ID. + /// + /// The ruleset's internal ID. + /// A ruleset, if available, else null. + public RulesetInfo GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.ID == id); + + /// + /// All available rulesets. + /// + public IEnumerable AvailableRulesets; + private static Assembly currentDomain_AssemblyResolve(object sender, ResolveEventArgs args) => loaded_assemblies.Keys.FirstOrDefault(a => a.FullName == args.Name); private const string ruleset_library_prefix = "osu.Game.Rulesets"; - protected override void Prepare(bool reset = false) + protected void AddMissingRulesets() { + var context = GetContext(); - Connection.CreateTable(); + var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, new RulesetInfo())).ToList(); - if (reset) + //add all legacy modes in correct order + foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID)) { - Connection.DeleteAll(); + var rulesetInfo = createRulesetInfo(r); + if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == rulesetInfo.ID) == null) + { + context.RulesetInfo.Add(rulesetInfo); + } } - var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, new RulesetInfo())); + context.SaveChanges(); - Connection.RunInTransaction(() => + //add any other modes + foreach (var r in instances.Where(r => r.LegacyID < 0)) { - //add all legacy modes in correct order - foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID)) - { - Connection.InsertOrReplace(createRulesetInfo(r)); - } + var us = createRulesetInfo(r); - //add any other modes - foreach (var r in instances.Where(r => r.LegacyID < 0)) - { - var us = createRulesetInfo(r); + var existing = context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == us.InstantiationInfo); - var existing = Query().Where(ri => ri.InstantiationInfo == us.InstantiationInfo).FirstOrDefault(); + if (existing == null) + context.RulesetInfo.Add(us); + } - if (existing == null) - Connection.Insert(us); - } - }); + context.SaveChanges(); - Connection.RunInTransaction(() => + //perform a consistency check + foreach (var r in context.RulesetInfo) { - //perform a consistency check - foreach (var r in Query()) + try { - try - { - r.CreateInstance(); - r.Available = true; - } - catch - { - r.Available = false; - } + var instance = r.CreateInstance(); - Connection.Update(r); + r.Name = instance.Description; + r.ShortName = instance.ShortName; + + r.Available = true; } - }); + catch + { + r.Available = false; + } + } + + context.SaveChanges(); + + AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList(); } private static void loadRulesetFromFile(string file) @@ -100,18 +113,17 @@ namespace osu.Game.Rulesets var assembly = Assembly.LoadFrom(file); loaded_assemblies[assembly] = assembly.GetTypes().First(t => t.IsSubclassOf(typeof(Ruleset))); } - catch (Exception) { } + catch (Exception) + { + } } private RulesetInfo createRulesetInfo(Ruleset ruleset) => new RulesetInfo { Name = ruleset.Description, + ShortName = ruleset.ShortName, InstantiationInfo = ruleset.GetType().AssemblyQualifiedName, ID = ruleset.LegacyID }; - - protected override Type[] ValidTypes => new[] { typeof(RulesetInfo) }; - - public RulesetInfo GetRuleset(int id) => Query().First(r => r.ID == id); } } diff --git a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs new file mode 100644 index 0000000000..4f603049db --- /dev/null +++ b/osu.Game/Rulesets/Scoring/PerformanceCalculator.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 System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Scoring +{ + public abstract class PerformanceCalculator + { + public abstract double Calculate(Dictionary categoryDifficulty = null); + } + + public abstract class PerformanceCalculator : PerformanceCalculator + where TObject : HitObject + { + private readonly Dictionary attributes = new Dictionary(); + protected IDictionary Attributes => attributes; + + protected readonly Beatmap Beatmap; + protected readonly Score Score; + + protected PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) + { + Beatmap = CreateBeatmapConverter().Convert(beatmap); + Score = score; + + var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); + diffCalc.Calculate(attributes); + } + + protected abstract BeatmapConverter CreateBeatmapConverter(); + } +} diff --git a/osu.Game/Rulesets/Scoring/Score.cs b/osu.Game/Rulesets/Scoring/Score.cs index 7f053ec1c5..6a06f364c6 100644 --- a/osu.Game/Rulesets/Scoring/Score.cs +++ b/osu.Game/Rulesets/Scoring/Score.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Scoring public double Health { get; set; } = 1; + public double? PP { get; set; } + public int MaxCombo { get; set; } public int Combo { get; set; } @@ -38,6 +40,6 @@ namespace osu.Game.Rulesets.Scoring public DateTimeOffset Date; - public Dictionary Statistics = new Dictionary(); + public Dictionary Statistics = new Dictionary(); } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index deb87e92d8..e129a81116 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Scoring /// public event Action NewJudgement; + /// + /// Additional conditions on top of that cause a failing state. + /// + public event Func FailConditions; + /// /// The current total score. /// @@ -51,6 +56,11 @@ namespace osu.Game.Rulesets.Scoring /// public readonly BindableInt Combo = new BindableInt(); + /// + /// The current rank. + /// + public readonly Bindable Rank = new Bindable(ScoreRank.X); + /// /// THe highest combo achieved by this score. /// @@ -61,19 +71,20 @@ namespace osu.Game.Rulesets.Scoring /// protected virtual bool HasCompleted => false; - /// - /// Whether the score is in a failed state. - /// - public virtual bool HasFailed => Health.Value == Health.MinValue; - /// /// Whether this ScoreProcessor has already triggered the failed state. /// - private bool alreadyFailed; + public virtual bool HasFailed { get; private set; } + + /// + /// The default conditions for failing. + /// + protected virtual bool DefaultFailCondition => Health.Value == Health.MinValue; protected ScoreProcessor() { Combo.ValueChanged += delegate { HighestCombo.Value = Math.Max(HighestCombo.Value, Combo.Value); }; + Accuracy.ValueChanged += delegate { Rank.Value = rankFrom(Accuracy.Value); }; } private ScoreRank rankFrom(double acc) @@ -101,9 +112,10 @@ namespace osu.Game.Rulesets.Scoring Accuracy.Value = 1; Health.Value = 1; Combo.Value = 0; + Rank.Value = ScoreRank.X; HighestCombo.Value = 0; - alreadyFailed = false; + HasFailed = false; } /// @@ -114,11 +126,14 @@ namespace osu.Game.Rulesets.Scoring /// protected void UpdateFailed() { - if (alreadyFailed || !HasFailed) + if (HasFailed) + return; + + if (!DefaultFailCondition && FailConditions?.Invoke(this) != true) return; if (Failed?.Invoke() != false) - alreadyFailed = true; + HasFailed = true; } /// @@ -142,7 +157,7 @@ namespace osu.Game.Rulesets.Scoring score.Combo = Combo; score.MaxCombo = HighestCombo; score.Accuracy = Accuracy; - score.Rank = rankFrom(Accuracy); + score.Rank = Rank; score.Date = DateTimeOffset.Now; score.Health = Health; } @@ -167,6 +182,7 @@ namespace osu.Game.Rulesets.Scoring private double maxBaseScore; private double rollingMaxBaseScore; private double baseScore; + private double bonusScore; protected ScoreProcessor() { @@ -177,6 +193,7 @@ namespace osu.Game.Rulesets.Scoring Debug.Assert(base_portion + combo_portion == 1.0); rulesetContainer.OnJudgement += AddJudgement; + rulesetContainer.OnJudgementRemoved += RemoveJudgement; SimulateAutoplay(rulesetContainer.Beatmap); Reset(true); @@ -205,14 +222,26 @@ namespace osu.Game.Rulesets.Scoring protected void AddJudgement(Judgement judgement) { OnNewJudgement(judgement); - NotifyNewJudgement(judgement); + updateScore(); UpdateFailed(); + NotifyNewJudgement(judgement); } + protected void RemoveJudgement(Judgement judgement) + { + OnJudgementRemoved(judgement); + updateScore(); + } + + /// + /// Applies a judgement. + /// + /// The judgement to apply/ protected virtual void OnNewJudgement(Judgement judgement) { - double bonusScore = 0; + judgement.ComboAtJudgement = Combo; + judgement.HighestComboAtJudgement = HighestCombo; if (judgement.AffectsCombo) { @@ -235,7 +264,30 @@ namespace osu.Game.Rulesets.Scoring } else if (judgement.IsHit) bonusScore += judgement.NumericResult; + } + /// + /// Removes a judgement. This should reverse everything in . + /// + /// The judgement to remove. + protected virtual void OnJudgementRemoved(Judgement judgement) + { + Combo.Value = judgement.ComboAtJudgement; + HighestCombo.Value = judgement.HighestComboAtJudgement; + + if (judgement.AffectsCombo) + { + baseScore -= judgement.NumericResult; + rollingMaxBaseScore -= judgement.MaxNumericResult; + + Hits--; + } + else if (judgement.IsHit) + bonusScore -= judgement.NumericResult; + } + + private void updateScore() + { if (rollingMaxBaseScore != 0) Accuracy.Value = baseScore / rollingMaxBaseScore; @@ -264,6 +316,7 @@ namespace osu.Game.Rulesets.Scoring Hits = 0; baseScore = 0; rollingMaxBaseScore = 0; + bonusScore = 0; } } diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index c06d31e38f..d8a79d8cfb 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.cs @@ -11,7 +11,6 @@ using osu.Game.IO.Legacy; using osu.Game.IPC; using osu.Game.Rulesets.Replays; using SharpCompress.Compressors.LZMA; -using SQLite.Net; namespace osu.Game.Rulesets.Scoring { @@ -27,7 +26,7 @@ namespace osu.Game.Rulesets.Scoring // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ScoreIPCChannel ipc; - public ScoreStore(Storage storage, SQLiteConnection connection, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(connection) + public ScoreStore(Storage storage, Func factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory) { this.storage = storage; this.beatmaps = beatmaps; @@ -144,11 +143,5 @@ namespace osu.Game.Rulesets.Scoring return new Replay { Frames = frames }; } - - protected override void Prepare(bool reset = false) - { - } - - protected override Type[] ValidTypes => new[] { typeof(Score) }; } } diff --git a/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs b/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs index f8e87bc022..b093cf3303 100644 --- a/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs +++ b/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Timing /// /// A which scrolls linearly relative to the start time. /// - internal class LinearScrollingContainer : ScrollingContainer + public class LinearScrollingContainer : ScrollingContainer { private readonly MultiplierControlPoint controlPoint; diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 47a39d4644..5ca3d9521b 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -6,18 +6,19 @@ using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using OpenTK; namespace osu.Game.Rulesets.UI { - public class ModIcon : Container + public class ModIcon : Container, IHasTooltip { private readonly SpriteIcon modIcon; private readonly SpriteIcon background; - private const float background_size = 80; + private const float size = 80; public FontAwesome Icon { @@ -27,29 +28,35 @@ namespace osu.Game.Rulesets.UI private readonly ModType type; + public string TooltipText { get; } + public ModIcon(Mod mod) { if (mod == null) throw new ArgumentNullException(nameof(mod)); type = mod.Type; + TooltipText = mod.Name; + + Size = new Vector2(size); + Children = new Drawable[] { background = new SpriteIcon { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Size = new Vector2(background_size), + Size = new Vector2(size), Icon = FontAwesome.fa_osu_mod_bg, + Y = -6.5f, Shadow = true, }, modIcon = new SpriteIcon { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, Colour = OsuColour.Gray(84), - Size = new Vector2(background_size - 35), - Y = 25, + Size = new Vector2(size - 35), Icon = mod.Icon }, }; diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 0270751946..b4a26344d5 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.UI /// public HitObjectContainer HitObjects { get; protected set; } - internal Container ScaledContent; + public Container ScaledContent; /// /// Whether we are currently providing the local user a gameplay cursor. diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 729df02ffd..fe7c0c05ed 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -55,15 +55,22 @@ namespace osu.Game.Rulesets.UI public abstract IEnumerable Objects { get; } + private readonly Lazy playfield; + /// + /// The playfield. + /// + public Playfield Playfield => playfield.Value; + protected readonly Ruleset Ruleset; /// /// A visual representation of a . /// /// The ruleset being repesented. - internal RulesetContainer(Ruleset ruleset) + protected RulesetContainer(Ruleset ruleset) { Ruleset = ruleset; + playfield = new Lazy(CreatePlayfield); } public abstract ScoreProcessor CreateScoreProcessor(); @@ -90,6 +97,12 @@ namespace osu.Game.Rulesets.UI Replay = replay; ReplayInputManager.ReplayInputHandler = replay != null ? CreateReplayInputHandler(replay) : null; } + + /// + /// Creates a Playfield. + /// + /// The Playfield. + protected abstract Playfield CreatePlayfield(); } /// @@ -104,6 +117,7 @@ namespace osu.Game.Rulesets.UI where TObject : HitObject { public event Action OnJudgement; + public event Action OnJudgementRemoved; /// /// The Beatmap @@ -134,11 +148,6 @@ namespace osu.Game.Rulesets.UI public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this); - /// - /// The playfield. - /// - public Playfield Playfield { get; private set; } - protected override Container Content => content; private Container content; @@ -172,11 +181,11 @@ namespace osu.Game.Rulesets.UI // Apply difficulty adjustments from mods before using Difficulty. foreach (var mod in Mods.OfType()) - mod.ApplyToDifficulty(Beatmap.BeatmapInfo.Difficulty); + mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty); // Apply defaults foreach (var h in Beatmap.HitObjects) - h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.Difficulty); + h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty); // Post-process the beatmap processor.PostProcess(Beatmap); @@ -197,7 +206,7 @@ namespace osu.Game.Rulesets.UI }); AddInternal(KeyBindingInputManager); - KeyBindingInputManager.Add(Playfield = CreatePlayfield()); + KeyBindingInputManager.Add(Playfield); loadObjects(); } @@ -211,7 +220,11 @@ namespace osu.Game.Rulesets.UI if (mods == null) return; - foreach (var mod in mods.OfType>()) + foreach (var mod in mods.OfType>()) + foreach (var obj in Beatmap.HitObjects) + mod.ApplyToHitObject(obj); + + foreach (var mod in mods.OfType>()) mod.ApplyToRulesetContainer(this); } @@ -241,6 +254,8 @@ namespace osu.Game.Rulesets.UI OnJudgement?.Invoke(j); }; + drawableObject.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(j); + Playfield.Add(drawableObject); } @@ -279,12 +294,6 @@ namespace osu.Game.Rulesets.UI /// The HitObject to make drawable. /// The DrawableHitObject. protected abstract DrawableHitObject GetVisualRepresentation(TObject h); - - /// - /// Creates a Playfield. - /// - /// The Playfield. - protected abstract Playfield CreatePlayfield(); } /// diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 0419070b42..5cd79cff29 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Timing; @@ -16,11 +18,24 @@ using OpenTK.Input; namespace osu.Game.Rulesets.UI { - public abstract class RulesetInputManager : DatabasedKeyBindingInputManager, ICanAttachKeyCounter, IHasReplayHandler + public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler where T : struct { - protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) : base(ruleset, variant, unique) + public class RulesetKeyBindingContainer : DatabasedKeyBindingInputManager { + public RulesetKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + } + + protected readonly KeyBindingContainer KeyBindingContainer; + + protected override Container Content => KeyBindingContainer; + + protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + { + InternalChild = KeyBindingContainer = new RulesetKeyBindingContainer(ruleset, variant, unique); } #region Action mapping (for replays) @@ -41,10 +56,10 @@ namespace osu.Game.Rulesets.UI List newActions = replayState.PressedActions; foreach (var released in lastPressedActions.Except(newActions)) - PropagateReleased(KeyBindingInputQueue, released); + KeyBindingContainer.TriggerReleased(released); foreach (var pressed in newActions.Except(lastPressedActions)) - PropagatePressed(KeyBindingInputQueue, pressed); + KeyBindingContainer.TriggerPressed(pressed); lastPressedActions = newActions; } @@ -76,6 +91,8 @@ namespace osu.Game.Rulesets.UI #region Clock control + protected override bool ShouldProcessClock => false; // We handle processing the clock ourselves + private ManualClock clock; private IFrameBasedClock parentClock; @@ -151,6 +168,12 @@ namespace osu.Game.Rulesets.UI } requireMoreUpdateLoops = clock.CurrentTime != parentClock.CurrentTime; + + // The manual clock time has changed in the above code. The framed clock now needs to be updated + // to ensure that the its time is valid for our children before input is processed + Clock.ProcessFrame(); + + // Process input base.Update(); } @@ -195,7 +218,7 @@ namespace osu.Game.Rulesets.UI Add(receptor); keyCounter.SetReceptor(receptor); - keyCounter.AddRange(DefaultKeyBindings.Select(b => b.GetAction()).Distinct().Select(b => new KeyCounterAction(b))); + keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings.Select(b => b.GetAction()).Distinct().Select(b => new KeyCounterAction(b))); } public class ActionReceptor : KeyCounterCollection.Receptor, IKeyBindingHandler diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs index 29a422892f..7ae3f6931e 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs @@ -32,9 +32,7 @@ namespace osu.Game.Screens.Backgrounds Schedule(() => { - var newBackground = new BeatmapBackground(beatmap); - - LoadComponentAsync(newBackground, delegate + LoadComponentAsync(new BeatmapBackground(beatmap), b => { float newDepth = 0; if (background != null) @@ -45,8 +43,8 @@ namespace osu.Game.Screens.Backgrounds background.Expire(); } - newBackground.Depth = newDepth; - Add(background = newBackground); + b.Depth = newDepth; + Add(background = b); background.BlurSigma = blurTarget; }); }); diff --git a/osu.Game/Screens/Charts/ChartInfo.cs b/osu.Game/Screens/Charts/ChartInfo.cs index b5ac5e4945..32577f3e05 100644 --- a/osu.Game/Screens/Charts/ChartInfo.cs +++ b/osu.Game/Screens/Charts/ChartInfo.cs @@ -3,7 +3,7 @@ namespace osu.Game.Screens.Charts { - internal class ChartInfo : ScreenWhiteBox + public class ChartInfo : ScreenWhiteBox { } } diff --git a/osu.Game/Screens/Charts/ChartListing.cs b/osu.Game/Screens/Charts/ChartListing.cs index 7bc6f0fa03..41c2a01600 100644 --- a/osu.Game/Screens/Charts/ChartListing.cs +++ b/osu.Game/Screens/Charts/ChartListing.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace osu.Game.Screens.Charts { - internal class ChartListing : ScreenWhiteBox + public class ChartListing : ScreenWhiteBox { protected override IEnumerable PossibleChildren => new[] { typeof(ChartInfo) diff --git a/osu.Game/Screens/Direct/OnlineListing.cs b/osu.Game/Screens/Direct/OnlineListing.cs index 9ce23c2863..ff6c599e6f 100644 --- a/osu.Game/Screens/Direct/OnlineListing.cs +++ b/osu.Game/Screens/Direct/OnlineListing.cs @@ -3,7 +3,7 @@ namespace osu.Game.Screens.Direct { - internal class OnlineListing : ScreenWhiteBox + public class OnlineListing : ScreenWhiteBox { } } diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs new file mode 100644 index 0000000000..0e57407928 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; + +namespace osu.Game.Screens.Edit.Components +{ + public class BottomBarContainer : Container + { + private const float corner_radius = 5; + private const float contents_padding = 15; + + public readonly Bindable Beatmap = new Bindable(); + protected Track Track => Beatmap.Value.Track; + + private readonly Drawable background; + private readonly Container content; + + protected override Container Content => content; + + public BottomBarContainer() + { + Masking = true; + CornerRadius = corner_radius; + + InternalChildren = new[] + { + background = new Box { RelativeSizeAxes = Axes.Both }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = contents_padding }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Gray1; + } + } +} diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs new file mode 100644 index 0000000000..5ffa66c43e --- /dev/null +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -0,0 +1,154 @@ +// 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.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +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.Screens.Edit.Components +{ + public class PlaybackControl : BottomBarContainer + { + private readonly IconButton playButton; + + public PlaybackControl() + { + PlaybackTabControl tabs; + + Children = new Drawable[] + { + playButton = new IconButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Scale = new Vector2(1.4f), + IconScale = new Vector2(1.4f), + Icon = FontAwesome.fa_play_circle_o, + Action = togglePause, + Padding = new MarginPadding { Left = 20 } + }, + new OsuSpriteText + { + Origin = Anchor.BottomLeft, + Text = "Playback Speed", + RelativePositionAxes = Axes.Y, + Y = 0.5f, + Padding = new MarginPadding { Left = 45 } + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + Padding = new MarginPadding { Left = 45 }, + Child = tabs = new PlaybackTabControl(), + } + }; + + tabs.Current.ValueChanged += newValue => Track.Tempo.Value = newValue; + } + + private void togglePause() + { + if (Track.IsRunning) + Track.Stop(); + else + Track.Start(); + } + + protected override void Update() + { + base.Update(); + + playButton.Icon = Track.IsRunning ? FontAwesome.fa_pause_circle_o : FontAwesome.fa_play_circle_o; + } + + private class PlaybackTabControl : OsuTabControl + { + private static readonly double[] tempo_values = { 0.5, 0.75, 1 }; + + protected override TabItem CreateTabItem(double value) => new PlaybackTabItem(value); + + protected override Dropdown CreateDropdown() => null; + + public PlaybackTabControl() + { + RelativeSizeAxes = Axes.Both; + TabContainer.Spacing = Vector2.Zero; + + tempo_values.ForEach(AddItem); + } + + public class PlaybackTabItem : TabItem + { + private const float fade_duration = 200; + + private readonly OsuSpriteText text; + private readonly OsuSpriteText textBold; + + public PlaybackTabItem(double value) : base(value) + { + RelativeSizeAxes = Axes.Both; + + Width = 1f / tempo_values.Length; + + Children = new Drawable[] + { + text = new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = $"{value:0%}", + TextSize = 14, + }, + textBold = new OsuSpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Text = $"{value:0%}", + TextSize = 14, + Font = @"Exo2.0-Bold", + Alpha = 0, + }, + }; + } + + private Color4 hoveredColour; + private Color4 normalColour; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + text.Colour = normalColour = colours.YellowDarker; + textBold.Colour = hoveredColour = colours.Yellow; + } + + protected override bool OnHover(InputState state) + { + updateState(); + return true; + } + + protected override void OnHoverLost(InputState state) => updateState(); + protected override void OnActivated() => updateState(); + protected override void OnDeactivated() => updateState(); + + private void updateState() + { + text.FadeColour(Active || IsHovered ? hoveredColour : normalColour, fade_duration, Easing.OutQuint); + text.FadeTo(Active ? 0 : 1, fade_duration, Easing.OutQuint); + textBold.FadeTo(Active ? 1 : 0, fade_duration, Easing.OutQuint); + } + } + } + } +} diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs new file mode 100644 index 0000000000..b9b6867ea6 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.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.Graphics; +using osu.Game.Graphics.Sprites; +using System; + +namespace osu.Game.Screens.Edit.Components +{ + public class TimeInfoContainer : BottomBarContainer + { + private const int count_duration = 150; + + private readonly OsuSpriteText trackTimer; + + public TimeInfoContainer() + { + Children = new Drawable[] + { + trackTimer = new OsuSpriteText + { + Origin = Anchor.BottomLeft, + RelativePositionAxes = Axes.Y, + TextSize = 22, + FixedWidth = true, + Y = 0.5f, + } + }; + } + + protected override void Update() + { + base.Update(); + + trackTimer.Text = TimeSpan.FromMilliseconds(Track.CurrentTime).ToString(@"mm\:ss\:fff"); + } + } +} diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs index 8afec62a08..cdb2985473 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs @@ -11,10 +11,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// /// The part of the timeline that displays bookmarks. /// - internal class BookmarkPart : TimelinePart + public class BookmarkPart : TimelinePart { protected override void LoadBeatmap(WorkingBeatmap beatmap) { + base.LoadBeatmap(beatmap); foreach (int bookmark in beatmap.BeatmapInfo.Bookmarks) Add(new BookmarkVisualisation(bookmark)); } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index 721825270b..380f8e2c7b 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -12,10 +12,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// /// The part of the timeline that displays breaks in the song. /// - internal class BreakPart : TimelinePart + public class BreakPart : TimelinePart { protected override void LoadBeatmap(WorkingBeatmap beatmap) { + base.LoadBeatmap(beatmap); foreach (var breakPeriod in beatmap.Beatmap.Breaks) Add(new BreakVisualisation(breakPeriod)); } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs index e7f4f03f9b..3759470c9a 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs @@ -14,16 +14,18 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// /// The part of the timeline that displays the control points. /// - internal class ControlPointPart : TimelinePart + public class ControlPointPart : TimelinePart { protected override void LoadBeatmap(WorkingBeatmap beatmap) { + base.LoadBeatmap(beatmap); + ControlPointInfo cpi = beatmap.Beatmap.ControlPointInfo; cpi.TimingPoints.ForEach(addTimingPoint); // Consider all non-timing points as the same type - cpi.SoundPoints.Select(c => (ControlPoint)c) + cpi.SamplePoints.Select(c => (ControlPoint)c) .Concat(cpi.EffectPoints) .Concat(cpi.DifficultyPoints) .Distinct() diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index d4a1177c4f..367cf4337d 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using osu.Game.Beatmaps; using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts @@ -14,7 +15,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// /// The part of the timeline that displays the current position of the song. /// - internal class MarkerPart : TimelinePart + public class MarkerPart : TimelinePart { private readonly Drawable marker; @@ -23,12 +24,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts Add(marker = new MarkerVisualisation()); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - marker.Colour = colours.Red; - } - protected override bool OnDragStart(InputState state) => true; protected override bool OnDragEnd(InputState state) => true; protected override bool OnDrag(InputState state) @@ -61,10 +56,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts protected override void Update() { base.Update(); - marker.X = (float)(Beatmap.Value?.Track.CurrentTime ?? 0); } + protected override void LoadBeatmap(WorkingBeatmap beatmap) + { + // block base call so we don't clear our marker (can be reused on beatmap change). + } + private class MarkerVisualisation : CompositeDrawable { public MarkerVisualisation() diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index f5d4124b19..df95a5c384 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// /// Represents a part of the summary timeline.. /// - internal abstract class TimelinePart : CompositeDrawable + public abstract class TimelinePart : CompositeDrawable { public Bindable Beatmap = new Bindable(); @@ -25,16 +25,30 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts Beatmap.ValueChanged += b => { - timeline.Clear(); - timeline.RelativeChildSize = new Vector2((float)Math.Max(1, b.Track.Length), 1); + updateRelativeChildSize(); LoadBeatmap(b); }; } + private void updateRelativeChildSize() + { + // the track may not be loaded completely (only has a length once it is). + if (!Beatmap.Value.Track.IsLoaded) + { + timeline.RelativeChildSize = Vector2.One; + Schedule(updateRelativeChildSize); + return; + } + + // Todo: This should be handled more gracefully + timeline.RelativeChildSize = Beatmap.Value.Track.Length == double.PositiveInfinity ? Vector2.One : new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); + } + protected void Add(Drawable visualisation) => timeline.Add(visualisation); protected virtual void LoadBeatmap(WorkingBeatmap beatmap) { + timeline.Clear(); } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index 4d925f7584..a63d02a0a5 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -3,11 +3,9 @@ using OpenTK; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -16,83 +14,64 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary /// /// The timeline that sits at the bottom of the editor. /// - public class SummaryTimeline : CompositeDrawable + public class SummaryTimeline : BottomBarContainer { - private const float corner_radius = 5; - private const float contents_padding = 15; - - public Bindable Beatmap = new Bindable(); - - private readonly Drawable background; - private readonly Drawable timelineBar; public SummaryTimeline() { - Masking = true; - CornerRadius = corner_radius; - TimelinePart markerPart, controlPointPart, bookmarkPart, breakPart; - InternalChildren = new[] + Children = new[] { - background = new Box { RelativeSizeAxes = Axes.Both }, - new Container + markerPart = new MarkerPart { RelativeSizeAxes = Axes.Both }, + controlPointPart = new ControlPointPart + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.Both, + Height = 0.35f + }, + bookmarkPart = new BookmarkPart + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Height = 0.35f + }, + timelineBar = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = contents_padding, Right = contents_padding }, - Children = new[] + Children = new Drawable[] { - markerPart = new MarkerPart { RelativeSizeAxes = Axes.Both }, - controlPointPart = new ControlPointPart + new Circle { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.Both, - Height = 0.35f + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Size = new Vector2(5) }, - bookmarkPart = new BookmarkPart + new Box { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Both, - Height = 0.35f + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Height = 1, + EdgeSmoothness = new Vector2(0, 1), }, - timelineBar = new Container + new Circle { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Circle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreRight, - Size = new Vector2(5) - }, - new Box - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - Height = 1, - EdgeSmoothness = new Vector2(0, 1), - }, - new Circle - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreLeft, - Size = new Vector2(5) - }, - } + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreLeft, + Size = new Vector2(5) }, - breakPart = new BreakPart - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Height = 0.25f - } } + }, + breakPart = new BreakPart + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Height = 0.25f } }; @@ -105,7 +84,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary [BackgroundDependencyLoader] private void load(OsuColour colours) { - background.Colour = colours.Gray1; timelineBar.Colour = colours.Gray5; } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs index aee8e250c3..91f5e9b222 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/DurationVisualisation.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations /// /// Represents a spanning point on a timeline part. /// - internal class DurationVisualisation : Container + public class DurationVisualisation : Container { protected DurationVisualisation(double startTime, double endTime) { diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs index 9d7272808b..4719db37d1 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations /// /// Represents a singular point on a timeline part. /// - internal class PointVisualisation : Box + public class PointVisualisation : Box { protected PointVisualisation(double startTime) { diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3ffd7754c1..19d00f3477 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -8,224 +8,128 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Menus; using osu.Game.Screens.Edit.Components.Timelines.Summary; -using OpenTK; using osu.Framework.Allocation; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Screens; +using osu.Game.Screens.Edit.Screens.Compose; +using osu.Game.Screens.Edit.Screens.Design; +using osu.Game.Screens.Edit.Components; namespace osu.Game.Screens.Edit { - internal class Editor : OsuScreen + public class Editor : OsuScreen { protected override BackgroundScreen CreateBackground() => new BackgroundScreenCustom(@"Backgrounds/bg4"); - internal override bool ShowOverlays => false; + public override bool ShowOverlays => false; private readonly Box bottomBackground; + private readonly Container screenContainer; + + private EditorScreen currentScreen; public Editor() { - Add(new Container + EditorMenuBar menuBar; + TimeInfoContainer timeInfo; + SummaryTimeline timeline; + PlaybackControl playback; + + Children = new[] { - RelativeSizeAxes = Axes.X, - Height = 40, - Children = new Drawable[] + new Container { - new Box + Name = "Screen container", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 40, Bottom = 60 }, + Child = screenContainer = new Container { RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex("111") - }, - new EditorMenuBar + Masking = true + } + }, + new Container + { + Name = "Top bar", + RelativeSizeAxes = Axes.X, + Height = 40, + Child = menuBar = new EditorMenuBar { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - X = 100, + RelativeSizeAxes = Axes.Both, Items = new[] { - new EditorMenuBarItem("File") + new MenuItem("File") { Items = new[] { - new EditorMenuItem("Clear all notes"), - new EditorMenuItem("Open difficulty..."), - new EditorMenuItem("Save"), - new EditorMenuItem("Create new difficulty..."), - new EditorMenuItemSpacer(), - new EditorMenuItem("Revert to saved"), - new EditorMenuItem("Revert to saved (full"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Test beatmap"), - new EditorMenuItem("Open AiMod"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Upload Beatmap..."), - new EditorMenuItem("Export package"), - new EditorMenuItem("Export map package"), - new EditorMenuItem("Import from..."), - new EditorMenuItemSpacer(), - new EditorMenuItem("Open song folder"), - new EditorMenuItem("Open .osu in Notepad"), - new EditorMenuItem("Open .osb in Notepad"), + new EditorMenuItem("Export", MenuItemType.Standard, exportBeatmap), new EditorMenuItemSpacer(), new EditorMenuItem("Exit", MenuItemType.Standard, Exit) } - }, - new EditorMenuBarItem("Edit") - { - Items = new[] - { - new EditorMenuItem("Undo"), - new EditorMenuItem("Redo"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Cut"), - new EditorMenuItem("Copy"), - new EditorMenuItem("Paste"), - new EditorMenuItem("Delete"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Select all"), - new EditorMenuItem("Clone"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Reverse selection"), - new EditorMenuItem("Flip horizontally"), - new EditorMenuItem("Flip vertically"), - new EditorMenuItem("Rotate 90deg clockwise"), - new EditorMenuItem("Rotate 90deg anticlockwise"), - new EditorMenuItem("Rotate by..."), - new EditorMenuItem("Scale by..."), - new EditorMenuItemSpacer(), - new EditorMenuItem("Reset selected objects' samples"), - new EditorMenuItem("Reset all samples", MenuItemType.Destructive), - new EditorMenuItem("Reset combo colours", MenuItemType.Destructive), - new EditorMenuItem("Reset breaks", MenuItemType.Destructive), - new EditorMenuItemSpacer(), - new EditorMenuItem("Nudge backward"), - new EditorMenuItem("Nudge forward") - } - }, - new EditorMenuBarItem("View") - { - Items = new[] - { - new EditorMenuItem("Compose"), - new EditorMenuItem("Design"), - new EditorMenuItem("Timing"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Song setup..."), - new EditorMenuItem("Timing setup..."), - new EditorMenuItemSpacer(), - new EditorMenuItem("Volume"), - new EditorMenuItem("Grid level"), - new EditorMenuItem("Show video"), - new EditorMenuItem("Show sample name"), - new EditorMenuItem("Snaking sliders"), - new EditorMenuItem("Hit animations"), - new EditorMenuItem("Follow points"), - new EditorMenuItem("Stacking") - } - }, - new EditorMenuBarItem("Compose") - { - Items = new[] - { - new EditorMenuItem("Snap divisor"), - new EditorMenuItem("Audio rate"), - new EditorMenuItem("Grid snapping"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Create polygon cricles..."), - new EditorMenuItem("Convert slider to stream"), - new EditorMenuItem("Enable live mapping mode"), - new EditorMenuItem("Sample import") - } - }, - new EditorMenuBarItem("Design") - { - Items = new[] - { - new EditorMenuItem("Move all elements in time...") - } - }, - new EditorMenuBarItem("Timing") - { - Items = new[] - { - new EditorMenuItem("Time signature"), - new EditorMenuItem("Metronome clicks"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Add timing section"), - new EditorMenuItem("Add inheriting section"), - new EditorMenuItem("Reset current section"), - new EditorMenuItem("Delete timing section"), - new EditorMenuItem("Resnap current section"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Timing setup..."), - new EditorMenuItemSpacer(), - new EditorMenuItem("Resnap all notes", MenuItemType.Destructive), - new EditorMenuItem("Move all notes in time...", MenuItemType.Destructive), - new EditorMenuItem("Recalculate slider lengths", MenuItemType.Destructive), - new EditorMenuItem("Delete all timing sections", MenuItemType.Destructive), - new EditorMenuItemSpacer(), - new EditorMenuItem("Set current position as preview point") - } - }, - new EditorMenuBarItem("Web") - { - Items = new[] - { - new EditorMenuItem("This Beatmap's information page"), - new EditorMenuItem("This Beatmap's thread"), - new EditorMenuItem("Quick reply") - } - }, - new EditorMenuBarItem("Help") - { - Items = new[] - { - new EditorMenuItem("Show in-game help"), - new EditorMenuItem("View FAQ") - } } } } - } - }); - - SummaryTimeline summaryTimeline; - Add(new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = 60, - Children = new Drawable[] + }, + new Container { - bottomBackground = new Box { RelativeSizeAxes = Axes.Both }, - new Container + Name = "Bottom bar", + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = 60, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Top = 5, Bottom = 5, Left = 10, Right = 10 }, - Child = new FillFlowContainer + bottomBackground = new Box { RelativeSizeAxes = Axes.Both }, + new Container { - Name = "Bottom bar", RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new[] + Padding = new MarginPadding { Vertical = 5, Horizontal = 10 }, + Child = new GridContainer { - summaryTimeline = new SummaryTimeline + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Width = 0.65f + new Dimension(GridSizeMode.Absolute, 220), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 220) + }, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 10 }, + Child = timeInfo = new TimeInfoContainer { RelativeSizeAxes = Axes.Both }, + }, + timeline = new SummaryTimeline + { + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 10 }, + Child = playback = new PlaybackControl { RelativeSizeAxes = Axes.Both }, + } + }, } - } + }, } } - } - }); + }, + }; - summaryTimeline.Beatmap.BindTo(Beatmap); + timeInfo.Beatmap.BindTo(Beatmap); + timeline.Beatmap.BindTo(Beatmap); + playback.Beatmap.BindTo(Beatmap); + menuBar.Mode.ValueChanged += onModeChanged; } [BackgroundDependencyLoader] @@ -234,6 +138,32 @@ namespace osu.Game.Screens.Edit bottomBackground.Colour = colours.Gray2; } + private void exportBeatmap() + { + Beatmap.Value.Save(); + } + + private void onModeChanged(EditorScreenMode mode) + { + currentScreen?.Exit(); + + switch (mode) + { + case EditorScreenMode.Compose: + currentScreen = new Compose(); + break; + case EditorScreenMode.Design: + currentScreen = new Design(); + break; + default: + currentScreen = new EditorScreen(); + break; + } + + currentScreen.Beatmap.BindTo(Beatmap); + screenContainer.Add(currentScreen); + } + protected override void OnResuming(Screen last) { Beatmap.Value.Track?.Stop(); @@ -250,7 +180,11 @@ namespace osu.Game.Screens.Edit protected override bool OnExiting(Screen next) { Background.FadeColour(Color4.White, 500); - Beatmap.Value.Track?.Start(); + if (Beatmap.Value.Track != null) + { + Beatmap.Value.Track.Tempo.Value = 1; + Beatmap.Value.Track.Start(); + } return base.OnExiting(next); } } diff --git a/osu.Game/Screens/Edit/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Menus/EditorMenuBar.cs index bb349b1531..17b2e64d8e 100644 --- a/osu.Game/Screens/Edit/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Menus/EditorMenuBar.cs @@ -11,16 +11,42 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using OpenTK; using OpenTK.Graphics; +using osu.Framework.Configuration; +using osu.Game.Screens.Edit.Screens; namespace osu.Game.Screens.Edit.Menus { public class EditorMenuBar : OsuMenu { + public readonly Bindable Mode = new Bindable(); + public EditorMenuBar() : base(Direction.Horizontal, true) { - ItemsContainer.Padding = new MarginPadding(0); - BackgroundColour = Color4.Transparent; + RelativeSizeAxes = Axes.X; + + MaskingContainer.CornerRadius = 0; + ItemsContainer.Padding = new MarginPadding { Left = 100 }; + BackgroundColour = OsuColour.FromHex("111"); + + ScreenSelectionTabControl tabControl; + AddRangeInternal(new Drawable[] + { + tabControl = new ScreenSelectionTabControl + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + X = -15 + } + }); + + Mode.BindTo(tabControl.Current); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Mode.TriggerChange(); } protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); @@ -29,27 +55,35 @@ namespace osu.Game.Screens.Edit.Menus private class DrawableEditorBarMenuItem : DrawableOsuMenuItem { - private Color4 openedForegroundColour; - private Color4 openedBackgroundColour; + private BackgroundBox background; public DrawableEditorBarMenuItem(MenuItem item) : base(item) { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + StateChanged += stateChanged; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - ForegroundColour = ForegroundColourHover = colours.BlueLight; - BackgroundColour = BackgroundColourHover = Color4.Transparent; - openedForegroundColour = Color4.White; - openedBackgroundColour = colours.Gray3; + ForegroundColour = colours.BlueLight; + BackgroundColour = Color4.Transparent; + ForegroundColourHover = Color4.White; + BackgroundColourHover = colours.Gray3; + } + + public override void SetFlowDirection(Direction direction) + { + AutoSizeAxes = Axes.Both; } protected override void UpdateBackgroundColour() { if (State == MenuItemState.Selected) - Background.FadeColour(openedBackgroundColour); + Background.FadeColour(BackgroundColourHover); else base.UpdateBackgroundColour(); } @@ -57,24 +91,58 @@ namespace osu.Game.Screens.Edit.Menus protected override void UpdateForegroundColour() { if (State == MenuItemState.Selected) - Foreground.FadeColour(openedForegroundColour); + Foreground.FadeColour(ForegroundColourHover); else base.UpdateForegroundColour(); } - protected override Drawable CreateBackground() => new Container + private void stateChanged(MenuItemState newState) { - RelativeSizeAxes = Axes.Both, - Masking = true, - Child = new Container + if (newState == MenuItemState.Selected) + background.Expand(); + else + background.Contract(); + } + + protected override Drawable CreateBackground() => background = new BackgroundBox(); + protected override DrawableOsuMenuItem.TextContainer CreateTextContainer() => new TextContainer(); + + private new class TextContainer : DrawableOsuMenuItem.TextContainer + { + public TextContainer() { - RelativeSizeAxes = Axes.Both, - Height = 2, - Masking = true, - CornerRadius = 4, - Child = new Box { RelativeSizeAxes = Axes.Both } + NormalText.TextSize = BoldText.TextSize = 14; + NormalText.Margin = BoldText.Margin = new MarginPadding { Horizontal = 10, Vertical = MARGIN_VERTICAL }; } - }; + } + + private class BackgroundBox : CompositeDrawable + { + private readonly Container innerBackground; + + public BackgroundBox() + { + RelativeSizeAxes = Axes.Both; + Masking = true; + InternalChild = innerBackground = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 4, + Child = new Box { RelativeSizeAxes = Axes.Both } + }; + } + + /// + /// Expands the background such that it doesn't show the bottom corners. + /// + public void Expand() => innerBackground.Height = 2; + + /// + /// Contracts the background such that it shows the bottom corners. + /// + public void Contract() => innerBackground.Height = 1; + } } private class SubMenu : OsuMenu diff --git a/osu.Game/Screens/Edit/Menus/EditorMenuBarItem.cs b/osu.Game/Screens/Edit/Menus/EditorMenuBarItem.cs deleted file mode 100644 index 201bc6e5c3..0000000000 --- a/osu.Game/Screens/Edit/Menus/EditorMenuBarItem.cs +++ /dev/null @@ -1,15 +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.UserInterface; - -namespace osu.Game.Screens.Edit.Menus -{ - public class EditorMenuBarItem : MenuItem - { - public EditorMenuBarItem(string text) - : base(text) - { - } - } -} diff --git a/osu.Game/Screens/Edit/Menus/ScreenSelectionTabControl.cs b/osu.Game/Screens/Edit/Menus/ScreenSelectionTabControl.cs new file mode 100644 index 0000000000..dae2e4b320 --- /dev/null +++ b/osu.Game/Screens/Edit/Menus/ScreenSelectionTabControl.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 OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Screens; +using OpenTK; + +namespace osu.Game.Screens.Edit.Menus +{ + public class ScreenSelectionTabControl : OsuTabControl + { + public ScreenSelectionTabControl() + { + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + TabContainer.RelativeSizeAxes &= ~Axes.X; + TabContainer.AutoSizeAxes = Axes.X; + TabContainer.Padding = new MarginPadding(); + + Add(new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = 1, + Colour = Color4.White.Opacity(0.2f), + }); + + Current.Value = EditorScreenMode.Compose; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AccentColour = colours.Yellow; + } + + protected override Dropdown CreateDropdown() => null; + + protected override TabItem CreateTabItem(EditorScreenMode value) => new TabItem(value); + + private class TabItem : OsuTabItem + { + private const float transition_length = 250; + + public TabItem(EditorScreenMode value) + : base(value) + { + Text.Margin = new MarginPadding(); + Text.Anchor = Anchor.CentreLeft; + Text.Origin = Anchor.CentreLeft; + } + + protected override void OnActivated() + { + base.OnActivated(); + Bar.ScaleTo(new Vector2(1, 5), transition_length, Easing.OutQuint); + } + + protected override void OnDeactivated() + { + base.OnDeactivated(); + Bar.ScaleTo(Vector2.One, transition_length, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs new file mode 100644 index 0000000000..6bc7356f26 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs @@ -0,0 +1,104 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Screens.Edit.Screens.Compose.Timeline; + +namespace osu.Game.Screens.Edit.Screens.Compose +{ + public class Compose : EditorScreen + { + private const float vertical_margins = 10; + private const float horizontal_margins = 20; + + private readonly Container composerContainer; + + public Compose() + { + ScrollableTimeline timeline; + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + Name = "Timeline", + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f) + }, + new Container + { + Name = "Timeline content", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 115 }, + Child = timeline = new ScrollableTimeline { RelativeSizeAxes = Axes.Both } + } + } + } + } + } + }, + new Drawable[] + { + composerContainer = new Container + { + Name = "Composer content", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins }, + } + } + }, + RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, 110) } + }, + }; + + timeline.Beatmap.BindTo(Beatmap); + Beatmap.ValueChanged += beatmapChanged; + } + + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + composerContainer.Clear(); + + var ruleset = newBeatmap.BeatmapInfo.Ruleset?.CreateInstance(); + if (ruleset == null) + { + Logger.Log("Beatmap doesn't have a ruleset assigned."); + // ExitRequested?.Invoke(); + return; + } + + var composer = ruleset.CreateHitObjectComposer(); + if (composer == null) + { + Logger.Log($"Ruleset {ruleset.Description} doesn't support hitobject composition."); + // ExitRequested?.Invoke(); + return; + } + + composerContainer.Child = composer; + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/DrawableRadioButton.cs b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/DrawableRadioButton.cs new file mode 100644 index 0000000000..10b6c07f3d --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/DrawableRadioButton.cs @@ -0,0 +1,123 @@ +// 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.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Screens.Compose.RadioButtons +{ + public class DrawableRadioButton : TriangleButton + { + /// + /// Invoked when this has been selected. + /// + public Action Selected; + + private Color4 defaultBackgroundColour; + private Color4 defaultBubbleColour; + private Color4 selectedBackgroundColour; + private Color4 selectedBubbleColour; + + private readonly Drawable bubble; + private readonly RadioButton button; + + public DrawableRadioButton(RadioButton button) + { + this.button = button; + + Text = button.Text; + Action = button.Action; + + RelativeSizeAxes = Axes.X; + + bubble = new CircularContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Scale = new Vector2(0.5f), + X = 10, + Masking = true, + Blending = BlendingMode.Additive, + Child = new Box { RelativeSizeAxes = Axes.Both } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + defaultBackgroundColour = colours.Gray3; + defaultBubbleColour = defaultBackgroundColour.Darken(0.5f); + selectedBackgroundColour = colours.BlueDark; + selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f); + + Triangles.Alpha = 0; + + Content.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 2, + Offset = new Vector2(0, 1), + Colour = Color4.Black.Opacity(0.5f) + }; + + Add(bubble); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + button.Selected.ValueChanged += v => + { + updateSelectionState(); + if (v) + Selected?.Invoke(button); + }; + + updateSelectionState(); + } + + private void updateSelectionState() + { + if (!IsLoaded) + return; + + BackgroundColour = button.Selected ? selectedBackgroundColour : defaultBackgroundColour; + bubble.Colour = button.Selected ? selectedBubbleColour : defaultBubbleColour; + } + + protected override bool OnClick(InputState state) + { + if (button.Selected) + return true; + + if (!Enabled) + return true; + + button.Selected.Value = true; + + return base.OnClick(state); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40f + }; + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButton.cs new file mode 100644 index 0000000000..055362d9e1 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButton.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Configuration; + +namespace osu.Game.Screens.Edit.Screens.Compose.RadioButtons +{ + public class RadioButton + { + /// + /// Whether this is selected. + /// + /// + public readonly BindableBool Selected; + + /// + /// The text that should be displayed in this button. + /// + public string Text; + + /// + /// The that should be invoked when this button is selected. + /// + public Action Action; + + public RadioButton(string text, Action action) + { + Text = text; + Action = action; + Selected = new BindableBool(); + } + + public RadioButton(string text) + : this(text, null) + { + Text = text; + Action = null; + } + + /// + /// Selects this . + /// + public void Select() => Selected.Value = true; + + /// + /// Deselects this . + /// + public void Deselect() => Selected.Value = false; + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButtonCollection.cs b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButtonCollection.cs new file mode 100644 index 0000000000..5f1def4a2e --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButtonCollection.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using OpenTK; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Edit.Screens.Compose.RadioButtons +{ + public class RadioButtonCollection : CompositeDrawable + { + private IReadOnlyList items; + public IReadOnlyList Items + { + get { return items; } + set + { + if (ReferenceEquals(items, value)) + return; + items = value; + + buttonContainer.Clear(); + items.ForEach(addButton); + } + } + + private readonly FlowContainer buttonContainer; + + public RadioButtonCollection() + { + AutoSizeAxes = Axes.Y; + + InternalChild = buttonContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5) + }; + } + + private RadioButton currentlySelected; + private void addButton(RadioButton button) + { + button.Selected.ValueChanged += v => + { + if (v) + { + currentlySelected?.Deselect(); + currentlySelected = button; + } + else + currentlySelected = null; + }; + + buttonContainer.Add(new DrawableRadioButton(button)); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/BeatmapWaveformGraph.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/BeatmapWaveformGraph.cs new file mode 100644 index 0000000000..5acee675e8 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/BeatmapWaveformGraph.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.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class BeatmapWaveformGraph : CompositeDrawable + { + public readonly Bindable Beatmap = new Bindable(); + + private readonly WaveformGraph graph; + + public BeatmapWaveformGraph() + { + InternalChild = graph = new WaveformGraph { RelativeSizeAxes = Axes.Both }; + Beatmap.ValueChanged += b => graph.Waveform = b.Waveform; + } + + /// + /// Gets or sets the . + /// + public float Resolution + { + get { return graph.Resolution; } + set { graph.Resolution = value; } + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollableTimeline.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollableTimeline.cs new file mode 100644 index 0000000000..81beb4a4de --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollableTimeline.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 OpenTK; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class ScrollableTimeline : CompositeDrawable + { + public readonly Bindable Beatmap = new Bindable(); + + private readonly ScrollingTimelineContainer timelineContainer; + + public ScrollableTimeline() + { + Masking = true; + CornerRadius = 5; + + OsuCheckbox hitObjectsCheckbox; + OsuCheckbox hitSoundsCheckbox; + OsuCheckbox waveformCheckbox; + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("111") + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("222") + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Y, + Width = 160, + Padding = new MarginPadding { Horizontal = 15 }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 4), + Children = new[] + { + hitObjectsCheckbox = new OsuCheckbox { LabelText = "Hitobjects" }, + hitSoundsCheckbox = new OsuCheckbox { LabelText = "Hitsounds" }, + waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" } + } + } + } + }, + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("333") + }, + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Masking = true, + Children = new[] + { + new TimelineButton + { + RelativeSizeAxes = Axes.Y, + Height = 0.5f, + Icon = FontAwesome.fa_search_plus, + Action = () => timelineContainer.Zoom++ + }, + new TimelineButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Y, + Height = 0.5f, + Icon = FontAwesome.fa_search_minus, + Action = () => timelineContainer.Zoom-- + }, + } + } + } + }, + timelineContainer = new ScrollingTimelineContainer { RelativeSizeAxes = Axes.Y } + } + } + }; + + hitObjectsCheckbox.Current.Value = true; + hitSoundsCheckbox.Current.Value = true; + waveformCheckbox.Current.Value = true; + + timelineContainer.Beatmap.BindTo(Beatmap); + timelineContainer.WaveformVisible.BindTo(waveformCheckbox.Current); + } + + protected override void Update() + { + base.Update(); + + timelineContainer.Size = new Vector2(DrawSize.X - timelineContainer.DrawPosition.X, 1); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollingTimelineContainer.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollingTimelineContainer.cs new file mode 100644 index 0000000000..3e1d0329a9 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollingTimelineContainer.cs @@ -0,0 +1,141 @@ +// 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 osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Graphics; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class ScrollingTimelineContainer : ScrollContainer + { + public readonly Bindable HitObjectsVisible = new Bindable(); + public readonly Bindable HitSoundsVisible = new Bindable(); + public readonly Bindable WaveformVisible = new Bindable(); + public readonly Bindable Beatmap = new Bindable(); + + private readonly BeatmapWaveformGraph waveform; + + public ScrollingTimelineContainer() + : base(Direction.Horizontal) + { + Masking = true; + + Add(waveform = new BeatmapWaveformGraph + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("222"), + Depth = float.MaxValue + }); + + Content.AutoSizeAxes = Axes.None; + Content.RelativeSizeAxes = Axes.Both; + + waveform.Beatmap.BindTo(Beatmap); + WaveformVisible.ValueChanged += waveformVisibilityChanged; + + Zoom = 10; + } + + private float minZoom = 1; + /// + /// The minimum zoom level allowed. + /// + public float MinZoom + { + get { return minZoom; } + set + { + if (value <= 0) + throw new ArgumentOutOfRangeException(nameof(value)); + if (minZoom == value) + return; + minZoom = value; + + // Update the zoom level + Zoom = Zoom; + } + } + + private float maxZoom = 30; + /// + /// The maximum zoom level allowed. + /// + public float MaxZoom + { + get { return maxZoom; } + set + { + if (value <= 0) + throw new ArgumentOutOfRangeException(nameof(value)); + if (maxZoom == value) + return; + maxZoom = value; + + // Update the zoom level + Zoom = Zoom; + } + } + + private float zoom = 1; + /// + /// The current zoom level. + /// + public float Zoom + { + get { return zoom; } + set + { + value = MathHelper.Clamp(value, MinZoom, MaxZoom); + if (zoom == value) + return; + zoom = value; + + // Make the zoom target default to the center of the graph if it hasn't been set + if (relativeContentZoomTarget == null) + relativeContentZoomTarget = ToSpaceOfOtherDrawable(DrawSize / 2, Content).X / Content.DrawSize.X; + if (localZoomTarget == null) + localZoomTarget = DrawSize.X / 2; + + Content.ResizeWidthTo(Zoom); + + // Update the scroll position to focus on the zoom target + float scrollPos = Content.DrawSize.X * relativeContentZoomTarget.Value - localZoomTarget.Value; + ScrollTo(scrollPos, false); + + relativeContentZoomTarget = null; + localZoomTarget = null; + } + } + + /// + /// Zoom target as a relative position in the space. + /// + private float? relativeContentZoomTarget; + + /// + /// Zoom target as a position in our local space. + /// + private float? localZoomTarget; + + protected override bool OnWheel(InputState state) + { + if (!state.Keyboard.ControlPressed) + return base.OnWheel(state); + + relativeContentZoomTarget = Content.ToLocalSpace(state.Mouse.NativeState.Position).X / Content.DrawSize.X; + localZoomTarget = ToLocalSpace(state.Mouse.NativeState.Position).X; + + Zoom += state.Mouse.WheelDelta; + + return true; + } + + private void waveformVisibilityChanged(bool visible) => waveform.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint); + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/TimelineButton.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/TimelineButton.cs new file mode 100644 index 0000000000..0c6fc5d133 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/TimelineButton.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 System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class TimelineButton : CompositeDrawable + { + public Action Action; + public readonly BindableBool Enabled = new BindableBool(true); + + public FontAwesome Icon + { + get { return button.Icon; } + set { button.Icon = value; } + } + + private readonly IconButton button; + + public TimelineButton() + { + InternalChild = button = new IconButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + IconColour = OsuColour.Gray(0.35f), + IconHoverColour = Color4.White, + HoverColour = OsuColour.Gray(0.25f), + FlashColour = OsuColour.Gray(0.5f), + Action = () => Action?.Invoke() + }; + + button.Enabled.BindTo(Enabled); + Width = button.ButtonSize.X; + } + + protected override void Update() + { + base.Update(); + + button.ButtonSize = new Vector2(button.ButtonSize.X, DrawHeight); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Design/Design.cs b/osu.Game/Screens/Edit/Screens/Design/Design.cs new file mode 100644 index 0000000000..edd5ae1a1e --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Design/Design.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.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Sprites; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Edit.Screens.Design +{ + public class Design : EditorScreen + { + public Design() + { + Add(new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.35f + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f + }, + new Container + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(20), + Child = new OsuSpriteText { Text = "Design screen" } + } + } + } + } + }); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/EditorScreen.cs b/osu.Game/Screens/Edit/Screens/EditorScreen.cs new file mode 100644 index 0000000000..ac248930d8 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/EditorScreen.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.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; + +namespace osu.Game.Screens.Edit.Screens +{ + public class EditorScreen : Container + { + public readonly Bindable Beatmap = new Bindable(); + + protected override Container Content => content; + private readonly Container content; + + public EditorScreen() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + + InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + this.FadeTo(0) + .Then() + .FadeTo(1f, 250, Easing.OutQuint); + } + + public void Exit() + { + this.FadeOut(250).Expire(); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/EditorScreenMode.cs b/osu.Game/Screens/Edit/Screens/EditorScreenMode.cs new file mode 100644 index 0000000000..578d888193 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/EditorScreenMode.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.ComponentModel; + +namespace osu.Game.Screens.Edit.Screens +{ + public enum EditorScreenMode + { + [Description("setup")] + SongSetup, + [Description("compose")] + Compose, + [Description("design")] + Design, + [Description("timing")] + Timing, + } +} diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index af084e740b..ec2e8e0cb1 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -1,27 +1,120 @@ // 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.Graphics; +using osu.Framework.Graphics.Shaders; using osu.Game.Screens.Menu; +using OpenTK; +using osu.Framework.Screens; namespace osu.Game.Screens { - internal class Loader : OsuScreen + public class Loader : OsuScreen { - internal override bool ShowOverlays => false; + private bool showDisclaimer; + + public override bool ShowOverlays => false; public Loader() { ValidForResume = false; } - [BackgroundDependencyLoader] - private void load(OsuGame game) + protected override void LogoArriving(OsuLogo logo, bool resuming) { - if (game.IsDeployedBuild) - LoadComponentAsync(new Disclaimer(), d => Push(d)); - else - LoadComponentAsync(new Intro(), d => Push(d)); + base.LogoArriving(logo, resuming); + + logo.Triangles = false; + logo.Origin = Anchor.BottomRight; + logo.Anchor = Anchor.BottomRight; + logo.Position = new Vector2(-40); + logo.Scale = new Vector2(0.2f); + + logo.FadeInFromZero(5000, Easing.OutQuint); + } + + private OsuScreen loadScreen; + private ShaderPrecompiler precompiler; + + protected override void OnEntering(Screen last) + { + base.OnEntering(last); + + LoadComponentAsync(precompiler = new ShaderPrecompiler(loadIfReady), Add); + LoadComponentAsync(loadScreen = showDisclaimer ? (OsuScreen)new Disclaimer() : new Intro(), s => loadIfReady()); + } + + private void loadIfReady() + { + if (ChildScreen == loadScreen) return; + + if (loadScreen.LoadState != LoadState.Ready) + return; + + if (!precompiler.FinishedCompiling) + return; + + Push(loadScreen); + } + + protected override void LogoSuspending(OsuLogo logo) + { + base.LogoSuspending(logo); + logo.FadeOut(100); + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase game) + { + showDisclaimer = game.IsDeployedBuild; + } + + /// + /// Compiles a set of shaders before continuing. Attempts to draw some frames between compilation by limiting to one compile per draw frame. + /// + public class ShaderPrecompiler : Drawable + { + private readonly Action onLoaded; + private readonly List loadTargets = new List(); + + public bool FinishedCompiling { get; private set; } + + public ShaderPrecompiler(Action onLoaded) + { + this.onLoaded = onLoaded; + } + + [BackgroundDependencyLoader] + private void load(ShaderManager manager) + { + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); + + loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); + + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE_ROUNDED)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); + } + + private Shader currentLoadTarget; + + protected override void Update() + { + base.Update(); + + // if our target is null we are done. + if (loadTargets.All(s => s.Loaded)) + { + FinishedCompiling = true; + Expire(); + onLoaded?.Invoke(); + } + } } } } diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs index ccd61643ce..5e55166c19 100644 --- a/osu.Game/Screens/Menu/Button.cs +++ b/osu.Game/Screens/Menu/Button.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(AudioManager audio) { - sampleHover = audio.Sample.Get(@"Menu/hover"); + sampleHover = audio.Sample.Get(@"Menu/button-hover"); if (!string.IsNullOrEmpty(sampleName)) sampleClick = audio.Sample.Get($@"Menu/{sampleName}"); } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index e4dbe00a80..ce7856c5a9 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -17,6 +17,7 @@ using OpenTK.Graphics; using OpenTK.Input; using osu.Framework.Audio.Sample; using osu.Framework.Audio; +using osu.Framework.Threading; namespace osu.Game.Screens.Menu { @@ -38,13 +39,28 @@ namespace osu.Game.Screens.Menu private readonly FlowContainerWithOrigin buttonFlow; //todo: make these non-internal somehow. - internal const float BUTTON_AREA_HEIGHT = 100; - internal const float BUTTON_WIDTH = 140f; - internal const float WEDGE_WIDTH = 20; + public const float BUTTON_AREA_HEIGHT = 100; - public const int EXIT_DELAY = 3000; + public const float BUTTON_WIDTH = 140f; + public const float WEDGE_WIDTH = 20; + + private OsuLogo logo; + + public void SetOsuLogo(OsuLogo logo) + { + this.logo = logo; + + if (this.logo != null) + { + this.logo.Action = onOsuLogo; + + // osuLogo.SizeForFlow relies on loading to be complete. + buttonFlow.Position = new Vector2(WEDGE_WIDTH * 2 - (BUTTON_WIDTH + this.logo.SizeForFlow / 4), 0); + + updateLogoState(); + } + } - private readonly OsuLogo osuLogo; private readonly Drawable iconFacade; private readonly Container buttonArea; private readonly Box buttonAreaBackground; @@ -99,21 +115,15 @@ namespace osu.Game.Screens.Menu } } }, - osuLogo = new OsuLogo - { - Action = onOsuLogo, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - } }; - buttonsPlay.Add(new Button(@"solo", @"select-6", FontAwesome.fa_user, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new Button(@"multi", @"select-5", FontAwesome.fa_users, new Color4(94, 63, 186, 255), () => OnMulti?.Invoke(), 0, Key.M)); - buttonsPlay.Add(new Button(@"chart", @"select-5", FontAwesome.fa_osu_charts, new Color4(80, 53, 160, 255), () => OnChart?.Invoke())); + buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.fa_user, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); + buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.fa_users, new Color4(94, 63, 186, 255), () => OnMulti?.Invoke(), 0, Key.M)); + buttonsPlay.Add(new Button(@"chart", @"button-generic-select", FontAwesome.fa_osu_charts, new Color4(80, 53, 160, 255), () => OnChart?.Invoke())); - buttonsTopLevel.Add(new Button(@"play", @"select-1", FontAwesome.fa_osu_logo, new Color4(102, 68, 204, 255), onPlay, WEDGE_WIDTH, Key.P)); - buttonsTopLevel.Add(new Button(@"osu!editor", @"select-5", FontAwesome.fa_osu_edit_o, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); - buttonsTopLevel.Add(new Button(@"osu!direct", string.Empty, FontAwesome.fa_osu_chevron_down_o, new Color4(165, 204, 0, 255), () => OnDirect?.Invoke(), 0, Key.D)); + buttonsTopLevel.Add(new Button(@"play", @"button-play-select", FontAwesome.fa_osu_logo, new Color4(102, 68, 204, 255), onPlay, WEDGE_WIDTH, Key.P)); + buttonsTopLevel.Add(new Button(@"osu!editor", @"button-generic-select", FontAwesome.fa_osu_edit_o, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); + buttonsTopLevel.Add(new Button(@"osu!direct", @"button-direct-select", FontAwesome.fa_osu_chevron_down_o, new Color4(165, 204, 0, 255), () => OnDirect?.Invoke(), 0, Key.D)); buttonsTopLevel.Add(new Button(@"exit", string.Empty, FontAwesome.fa_osu_cross_o, new Color4(238, 51, 153, 255), onExit, 0, Key.Q)); buttonFlow.AddRange(buttonsPlay); @@ -124,15 +134,7 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio, OsuGame game = null) { toolbar = game?.Toolbar; - sampleBack = audio.Sample.Get(@"Menu/select-4"); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - // osuLogo.SizeForFlow relies on loading to be complete. - buttonFlow.Position = new Vector2(WEDGE_WIDTH * 2 - (BUTTON_WIDTH + osuLogo.SizeForFlow / 4), 0); + sampleBack = audio.Sample.Get(@"Menu/button-back-select"); } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) @@ -142,7 +144,7 @@ namespace osu.Game.Screens.Menu switch (args.Key) { case Key.Space: - osuLogo.TriggerOnClick(state); + logo?.TriggerOnClick(state); return true; case Key.Escape: switch (State) @@ -178,19 +180,21 @@ namespace osu.Game.Screens.Menu State = MenuState.TopLevel; } - private void onOsuLogo() + private bool onOsuLogo() { switch (state) { + default: + return true; case MenuState.Initial: State = MenuState.TopLevel; - return; + return true; case MenuState.TopLevel: buttonsTopLevel.First().TriggerOnClick(); - return; + return false; case MenuState.Play: buttonsPlay.First().TriggerOnClick(); - return; + return false; } } @@ -215,12 +219,12 @@ namespace osu.Game.Screens.Menu backButton.ContractStyle = 0; settingsButton.ContractStyle = 0; - bool fromInitial = lastState == MenuState.Initial; - if (state == MenuState.TopLevel) buttonArea.FinishTransforms(true); - using (buttonArea.BeginDelayedSequence(fromInitial ? 150 : 0, true)) + updateLogoState(lastState); + + using (buttonArea.BeginDelayedSequence(lastState == MenuState.Initial ? 150 : 0, true)) { switch (state) { @@ -229,39 +233,18 @@ namespace osu.Game.Screens.Menu buttonAreaBackground.ScaleTo(Vector2.One, 500, Easing.Out); buttonArea.FadeOut(300); - osuLogo.Delay(150) - .Schedule(() => toolbar?.Hide()) - .ScaleTo(1, 800, Easing.OutExpo) - .MoveTo(Vector2.Zero, 800, Easing.OutExpo); - foreach (Button b in buttonsTopLevel) b.State = ButtonState.Contracted; foreach (Button b in buttonsPlay) b.State = ButtonState.Contracted; - if (state == MenuState.Exit) - { - osuLogo.RotateTo(20, EXIT_DELAY * 1.5f); - osuLogo.FadeOut(EXIT_DELAY); - } - else if (lastState == MenuState.TopLevel) + if (state != MenuState.Exit && lastState == MenuState.TopLevel) sampleBack?.Play(); break; case MenuState.TopLevel: buttonAreaBackground.ScaleTo(Vector2.One, 200, Easing.Out); - var sequence = osuLogo - .ScaleTo(0.5f, 200, Easing.In) - .MoveTo(buttonFlow.DrawPosition, 200, Easing.In); - - if (fromInitial && osuLogo.Scale.X > 0.5f) - sequence.OnComplete(o => - { - o.Impact(); - toolbar?.Show(); - }); - buttonArea.FadeIn(300); foreach (Button b in buttonsTopLevel) @@ -301,15 +284,84 @@ namespace osu.Game.Screens.Menu } } + private ScheduledDelegate logoDelayedAction; + + private void updateLogoState(MenuState lastState = MenuState.Initial) + { + if (logo == null) return; + + logoDelayedAction?.Cancel(); + + switch (state) + { + case MenuState.Exit: + case MenuState.Initial: + logoTracking = false; + + logoDelayedAction = Scheduler.AddDelayed(() => + { + toolbar?.Hide(); + + logo.ClearTransforms(targetMember: nameof(Position)); + logo.RelativePositionAxes = Axes.Both; + + logo.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo); + logo.ScaleTo(1, 800, Easing.OutExpo); + }, 150); + + break; + case MenuState.TopLevel: + case MenuState.Play: + logo.ClearTransforms(targetMember: nameof(Position)); + logo.RelativePositionAxes = Axes.None; + + switch (lastState) + { + case MenuState.TopLevel: // coming from toplevel to play + case MenuState.Initial: + logoTracking = false; + logo.ScaleTo(0.5f, 200, Easing.In); + + logo.MoveTo(logoTrackingPosition, lastState == MenuState.EnteringMode ? 0 : 200, Easing.In); + + logoDelayedAction = Scheduler.AddDelayed(() => + { + logoTracking = true; + + logo.Impact(); + toolbar?.Show(); + }, 200); + break; + default: + logoTracking = true; + logo.ScaleTo(0.5f, 200, Easing.OutQuint); + break; + } + break; + case MenuState.EnteringMode: + logoTracking = true; + break; + } + } + + private Vector2 logoTrackingPosition => logo.Parent.ToLocalSpace(iconFacade.ScreenSpaceDrawQuad.Centre); + + private bool logoTracking; + protected override void Update() { //if (OsuGame.IdleTime > 6000 && State != MenuState.Exit) // State = MenuState.Initial; - osuLogo.Interactive = Alpha > 0.2f; - - iconFacade.Width = osuLogo.SizeForFlow * 0.5f; base.Update(); + + if (logo != null) + { + if (logoTracking) + logo.Position = logoTrackingPosition; + + iconFacade.Width = logo.SizeForFlow * 0.5f; + } } } diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 1ac5823ec4..532ee71b72 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -12,15 +12,15 @@ using OpenTK.Graphics; namespace osu.Game.Screens.Menu { - internal class Disclaimer : OsuScreen + public class Disclaimer : OsuScreen { private Intro intro; private readonly SpriteIcon icon; private Color4 iconColour; - internal override bool ShowOverlays => false; + public override bool ShowOverlays => false; - internal override bool HasLocalCursorDisplayed => true; + public override bool HasLocalCursorDisplayed => true; public Disclaimer() { diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs index 3aeef4bbc9..d7beb34a2f 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/Intro.cs @@ -12,56 +12,31 @@ using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.IO; using osu.Game.Configuration; -using osu.Game.Graphics.Containers; using osu.Game.Screens.Backgrounds; +using OpenTK; using OpenTK.Graphics; namespace osu.Game.Screens.Menu { public class Intro : OsuScreen { - private readonly OsuLogo logo; - - private const string menu_music_beatmap_hash = "715a09144f885d746644c1983e285044"; + private const string menu_music_beatmap_hash = "3c8b1fcc9434dbb29e2fb613d3b9eada9d7bb6c125ceb32396c3b53437280c83"; /// /// Whether we have loaded the menu previously. /// - internal bool DidLoadMenu; + public bool DidLoadMenu; private MainMenu mainMenu; private SampleChannel welcome; private SampleChannel seeya; - internal override bool HasLocalCursorDisplayed => true; + public override bool HasLocalCursorDisplayed => true; - internal override bool ShowOverlays => false; + public override bool ShowOverlays => false; protected override BackgroundScreen CreateBackground() => new BackgroundScreenEmpty(); - public Intro() - { - Children = new Drawable[] - { - new ParallaxContainer - { - ParallaxAmount = 0.01f, - Children = new Drawable[] - { - logo = new OsuLogo - { - Alpha = 0, - Triangles = false, - Blending = BlendingMode.Additive, - Interactive = false, - Colour = Color4.DarkGray, - Ripple = false - } - } - } - }; - } - private Bindable menuVoice; private Bindable menuMusic; private Track track; @@ -76,7 +51,7 @@ namespace osu.Game.Screens.Menu if (!menuMusic) { - var sets = beatmaps.GetAllUsableBeatmapSets(false); + var sets = beatmaps.GetAllUsableBeatmapSets(); if (sets.Count > 0) setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID); } @@ -89,9 +64,7 @@ namespace osu.Game.Screens.Menu { // we need to import the default menu background beatmap setInfo = beatmaps.Import(new OszArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"))); - setInfo.Protected = true; - beatmaps.Delete(setInfo); } } @@ -101,6 +74,9 @@ namespace osu.Game.Screens.Menu welcome = audio.Sample.Get(@"welcome"); seeya = audio.Sample.Get(@"seeya"); + + if (setInfo.Protected) + beatmaps.Delete(setInfo); } protected override void OnEntering(Screen last) @@ -122,14 +98,46 @@ namespace osu.Game.Screens.Menu { DidLoadMenu = true; Push(mainMenu); - }, 2300); - }, 600); + }, delay_step_one); + }, delay_step_two); + } - logo.ScaleTo(0.4f); - logo.FadeOut(); + private const double delay_step_one = 2300; + private const double delay_step_two = 600; - logo.ScaleTo(1, 4400, Easing.OutQuint); - logo.FadeIn(20000, Easing.OutQuint); + public const int EXIT_DELAY = 3000; + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + logo.RelativePositionAxes = Axes.Both; + logo.Colour = Color4.White; + logo.Ripple = false; + + const int quick_appear = 350; + + int initialMovementTime = logo.Alpha > 0.2f ? quick_appear : 0; + + logo.MoveTo(new Vector2(0.5f), initialMovementTime, Easing.OutQuint); + + if (!resuming) + { + logo.ScaleTo(1); + logo.FadeIn(); + logo.PlayIntro(); + } + else + { + logo.Triangles = false; + + logo + .ScaleTo(1, initialMovementTime, Easing.OutQuint) + .FadeIn(quick_appear, Easing.OutQuint) + .Then() + .RotateTo(20, EXIT_DELAY * 1.5f) + .FadeOut(EXIT_DELAY); + } } protected override void OnSuspending(Screen next) @@ -149,7 +157,7 @@ namespace osu.Game.Screens.Menu if (!(last is MainMenu)) Content.FadeIn(300); - double fadeOutTime = 2000; + double fadeOutTime = EXIT_DELAY; //we also handle the exit transition. if (menuVoice) seeya.Play(); diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs new file mode 100644 index 0000000000..5fca389708 --- /dev/null +++ b/osu.Game/Screens/Menu/IntroSequence.cs @@ -0,0 +1,297 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using OpenTK; +using 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.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Menu +{ + public class IntroSequence : Container + { + private const float logo_size = 460; //todo: this should probably be 480 + + private OsuSpriteText welcomeText; + + private Container lines; + + private Box lineTopLeft; + private Box lineBottomLeft; + private Box lineTopRight; + private Box lineBottomRight; + + private Ring smallRing; + private Ring mediumRing; + private Ring bigRing; + + private Box backgroundFill; + private Box foregroundFill; + + private CircularContainer pinkCircle; + private CircularContainer blueCircle; + private CircularContainer yellowCircle; + private CircularContainer purpleCircle; + + public IntroSequence() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + const int line_offset = 80; + const int circle_offset = 250; + + Children = new Drawable[] + { + lines = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new [] + { + lineTopLeft = new Box + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.Centre, + Position = new Vector2(-line_offset, -line_offset), + Rotation = 45, + Colour = Color4.White.Opacity(180), + }, + lineTopRight = new Box + { + Origin = Anchor.CentreRight, + Anchor = Anchor.Centre, + Position = new Vector2(line_offset, -line_offset), + Rotation = -45, + Colour = Color4.White.Opacity(80), + }, + lineBottomLeft = new Box + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.Centre, + Position = new Vector2(-line_offset, line_offset), + Rotation = -45, + Colour = Color4.White.Opacity(230), + }, + lineBottomRight = new Box + { + Origin = Anchor.CentreRight, + Anchor = Anchor.Centre, + Position = new Vector2(line_offset, line_offset), + Rotation = 45, + Colour = Color4.White.Opacity(130), + }, + } + }, + bigRing = new Ring(OsuColour.FromHex(@"B6C5E9"), 0.85f), + mediumRing = new Ring(Color4.White.Opacity(130), 0.7f), + smallRing = new Ring(Color4.White, 0.6f), + welcomeText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "welcome", + Padding = new MarginPadding { Bottom = 10 }, + Font = @"Exo2.0-Light", + Alpha = 0, + TextSize = 42, + Spacing = new Vector2(5), + }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(logo_size), + Masking = true, + Children = new Drawable[] + { + backgroundFill = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Height = 0, + Colour = OsuColour.FromHex(@"C6D8FF").Opacity(160), + }, + foregroundFill = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = Vector2.Zero, + RelativeSizeAxes = Axes.Both, + Width = 0, + Colour = Color4.White, + }, + } + }, + purpleCircle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Position = new Vector2(0, circle_offset), + Colour = OsuColour.FromHex(@"AA92FF"), + }, + blueCircle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Position = new Vector2(-circle_offset, 0), + Colour = OsuColour.FromHex(@"8FE5FE"), + }, + yellowCircle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Position = new Vector2(0, -circle_offset), + Colour = OsuColour.FromHex(@"FFD64C"), + }, + pinkCircle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Position = new Vector2(circle_offset, 0), + Colour = OsuColour.FromHex(@"e967a1"), + }, + }; + + foreach (var line in lines) + { + line.Size = new Vector2(105, 1.5f); + line.Alpha = 0; + } + + Scale = new Vector2(0.5f); + } + + public void Start(double length) + { + if (Children.Any()) + { + // restart if we were already run previously. + FinishTransforms(true); + load(); + } + + smallRing.ResizeTo(logo_size * 0.086f, 400, Easing.InOutQuint); + + mediumRing.ResizeTo(130, 340, Easing.OutQuad); + mediumRing.Foreground.ResizeTo(1, 880, Easing.Out); + + Func remainingTime = () => length - TransformDelay; + + using (BeginDelayedSequence(250, true)) + { + welcomeText.FadeIn(700); + welcomeText.TransformSpacingTo(new Vector2(20, 0), remainingTime(), Easing.Out); + + const int line_duration = 700; + const int line_resize = 150; + + foreach (var line in lines) + { + line.FadeIn(40).ResizeWidthTo(0, line_duration - line_resize, Easing.OutQuint); + } + + const int line_end_offset = 120; + + smallRing.Foreground.ResizeTo(1, line_duration, Easing.OutQuint); + + lineTopLeft.MoveTo(new Vector2(-line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); + lineTopRight.MoveTo(new Vector2(line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); + lineBottomLeft.MoveTo(new Vector2(-line_end_offset, line_end_offset), line_duration, Easing.OutQuint); + lineBottomRight.MoveTo(new Vector2(line_end_offset, line_end_offset), line_duration, Easing.OutQuint); + + using (BeginDelayedSequence(length * 0.56, true)) + { + bigRing.ResizeTo(logo_size, 500, Easing.InOutQuint); + bigRing.Foreground.Delay(250).ResizeTo(1, 850, Easing.OutQuint); + + using (BeginDelayedSequence(250, true)) + { + backgroundFill.ResizeHeightTo(1, remainingTime(), Easing.InOutQuart); + backgroundFill.RotateTo(-90, remainingTime(), Easing.InOutQuart); + + using (BeginDelayedSequence(50, true)) + { + foregroundFill.ResizeWidthTo(1, remainingTime(), Easing.InOutQuart); + foregroundFill.RotateTo(-90, remainingTime(), Easing.InOutQuart); + } + + this.ScaleTo(1, remainingTime(), Easing.InOutCubic); + + const float circle_size = logo_size * 0.9f; + + const int rotation_delay = 110; + const int appear_delay = 80; + + purpleCircle.MoveToY(circle_size / 2, remainingTime(), Easing.InOutQuart); + purpleCircle.Delay(rotation_delay).RotateTo(-180, remainingTime() - rotation_delay, Easing.InOutQuart); + purpleCircle.ResizeTo(circle_size, remainingTime(), Easing.InOutQuart); + + using (BeginDelayedSequence(appear_delay, true)) + { + yellowCircle.MoveToY(-circle_size / 2, remainingTime(), Easing.InOutQuart); + yellowCircle.Delay(rotation_delay).RotateTo(-180, remainingTime() - rotation_delay, Easing.InOutQuart); + yellowCircle.ResizeTo(circle_size, remainingTime(), Easing.InOutQuart); + + using (BeginDelayedSequence(appear_delay, true)) + { + blueCircle.MoveToX(-circle_size / 2, remainingTime(), Easing.InOutQuart); + blueCircle.Delay(rotation_delay).RotateTo(-180, remainingTime() - rotation_delay, Easing.InOutQuart); + blueCircle.ResizeTo(circle_size, remainingTime(), Easing.InOutQuart); + + using (BeginDelayedSequence(appear_delay, true)) + { + pinkCircle.MoveToX(circle_size / 2, remainingTime(), Easing.InOutQuart); + pinkCircle.Delay(rotation_delay).RotateTo(-180, remainingTime() - rotation_delay, Easing.InOutQuart); + pinkCircle.ResizeTo(circle_size, remainingTime(), Easing.InOutQuart); + } + } + } + } + } + } + } + + private class Ring : Container + { + public readonly Circle Foreground; + + public Ring(Color4 ringColour, float foregroundSize) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Children = new[] + { + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.98f), + Colour = ringColour, + }, + Foreground = new Circle + { + Size = new Vector2(foregroundSize), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + } + }; + } + } + } +} diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 4b8942349d..5b86fd6ca3 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -19,7 +19,7 @@ using osu.Framework.Allocation; namespace osu.Game.Screens.Menu { - internal class LogoVisualisation : Drawable, IHasAccentColour + public class LogoVisualisation : Drawable, IHasAccentColour { private readonly Bindable beatmap = new Bindable(); @@ -85,12 +85,11 @@ namespace osu.Game.Screens.Menu private void updateAmplitudes() { - var track = beatmap.Value.Track; + var track = beatmap.Value.TrackLoaded ? beatmap.Value.Track : null; + var effect = beatmap.Value.BeatmapLoaded ? beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current) : null; float[] temporalAmplitudes = track?.CurrentAmplitudes.FrequencyAmplitudes ?? new float[256]; - var effect = beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(track?.CurrentTime ?? Time.Current); - for (int i = 0; i < bars_per_visualiser; i++) { if (track?.IsRunning ?? false) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 1c82d15f50..90f68ba9f1 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using OpenTK; +using OpenTK.Graphics; using OpenTK.Input; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -23,7 +24,7 @@ namespace osu.Game.Screens.Menu { private readonly ButtonSystem buttons; - internal override bool ShowOverlays => buttons.State != MenuState.Initial; + public override bool ShowOverlays => buttons.State != MenuState.Initial; private readonly BackgroundScreenDefault background; private Screen songSelect; @@ -58,13 +59,16 @@ namespace osu.Game.Screens.Menu }; } - [BackgroundDependencyLoader] - private void load(OsuGame game) + [BackgroundDependencyLoader(true)] + private void load(OsuGame game = null) { LoadComponentAsync(background); - buttons.OnSettings = game.ToggleSettings; - buttons.OnDirect = game.ToggleDirect; + if (game != null) + { + buttons.OnSettings = game.ToggleSettings; + buttons.OnDirect = game.ToggleDirect; + } preloadSongSelect(); } @@ -102,6 +106,35 @@ namespace osu.Game.Screens.Menu Beatmap.ValueChanged += beatmap_ValueChanged; } + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + buttons.SetOsuLogo(logo); + + logo.FadeColour(Color4.White, 100, Easing.OutQuint); + logo.FadeIn(100, Easing.OutQuint); + + if (resuming) + { + buttons.State = MenuState.TopLevel; + + const float length = 300; + + Content.FadeIn(length, Easing.OutQuint); + Content.MoveTo(new Vector2(0, 0), length, Easing.OutQuint); + + sideFlashes.Delay(length).FadeIn(64, Easing.InQuint); + } + } + + protected override void LogoSuspending(OsuLogo logo) + { + logo.FadeOut(300, Easing.InSine) + .ScaleTo(0.2f, 300, Easing.InSine) + .OnComplete(l => buttons.SetOsuLogo(null)); + } + private void beatmap_ValueChanged(WorkingBeatmap newValue) { if (!IsCurrentScreen) @@ -121,7 +154,7 @@ namespace osu.Game.Screens.Menu Content.FadeOut(length, Easing.InSine); Content.MoveTo(new Vector2(-800, 0), length, Easing.InSine); - sideFlashes.FadeOut(length / 4, Easing.OutQuint); + sideFlashes.FadeOut(64, Easing.OutQuint); } protected override void OnResuming(Screen last) @@ -132,15 +165,6 @@ namespace osu.Game.Screens.Menu //we may have consumed our preloaded instance, so let's make another. preloadSongSelect(); - - const float length = 300; - - buttons.State = MenuState.TopLevel; - - Content.FadeIn(length, Easing.OutQuint); - Content.MoveTo(new Vector2(0, 0), length, Easing.OutQuint); - - sideFlashes.FadeIn(length / 4, Easing.InQuint); } protected override bool OnExiting(Screen next) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 6f4a46b10b..9ca12702e5 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -29,6 +29,8 @@ namespace osu.Game.Screens.Menu { public readonly Color4 OsuPink = OsuColour.FromHex(@"e967a1"); + private const double transition_length = 300; + private readonly Sprite logo; private readonly CircularContainer logoContainer; private readonly Container logoBounceContainer; @@ -37,6 +39,8 @@ namespace osu.Game.Screens.Menu private readonly Container logoHoverContainer; private readonly LogoVisualisation visualizer; + private readonly IntroSequence intro; + private SampleChannel sampleClick; private SampleChannel sampleBeat; @@ -44,7 +48,10 @@ namespace osu.Game.Screens.Menu private readonly Triangles triangles; - public Action Action; + /// + /// Return value decides whether the logo should play its own sample for the click action. + /// + public Func Action; public float SizeForFlow => logo == null ? 0 : logo.DrawSize.X * logo.Scale.X * logoBounceContainer.Scale.X * logoHoverContainer.Scale.X * 0.74f; @@ -54,7 +61,7 @@ namespace osu.Game.Screens.Menu public bool Triangles { - set { colourAndTriangles.Alpha = value ? 1 : 0; } + set { colourAndTriangles.FadeTo(value ? 1 : 0, transition_length, Easing.OutQuint); } } public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => logoContainer.ReceiveMouseInputAt(screenSpacePos); @@ -62,10 +69,9 @@ namespace osu.Game.Screens.Menu public bool Ripple { get { return rippleContainer.Alpha > 0; } - set { rippleContainer.Alpha = value ? 1 : 0; } + set { rippleContainer.FadeTo(value ? 1 : 0, transition_length, Easing.OutQuint); } } - public bool Interactive = true; private readonly Box flashLayer; private readonly Container impactContainer; @@ -76,17 +82,23 @@ namespace osu.Game.Screens.Menu public OsuLogo() { + // Required to make Schedule calls run in OsuScreen even when we are not visible. + AlwaysPresent = true; + EarlyActivationMilliseconds = early_activation; Size = new Vector2(default_size); - Anchor = Anchor.Centre; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; Children = new Drawable[] { + intro = new IntroSequence + { + RelativeSizeAxes = Axes.Both, + }, logoHoverContainer = new Container { AutoSizeAxes = Axes.Both, @@ -212,11 +224,35 @@ namespace osu.Game.Screens.Menu }; } + /// + /// Schedule a new extenral animation. Handled queueing and finishing previous animations in a sane way. + /// + /// The animation to be performed + /// If true, the new animation is delayed until all previous transforms finish. If false, existing transformed are cleared. + public void AppendAnimatingAction(Action action, bool waitForPrevious) + { + Action runnableAction = () => + { + if (waitForPrevious) + this.DelayUntilTransformsFinished().Schedule(action); + else + { + ClearTransforms(); + action(); + } + }; + + if (IsLoaded) + runnableAction(); + else + Schedule(() => runnableAction()); + } + [BackgroundDependencyLoader] private void load(TextureStore textures, AudioManager audio) { - sampleClick = audio.Sample.Get(@"Menu/select-2"); - sampleBeat = audio.Sample.Get(@"Menu/heartbeat"); + sampleClick = audio.Sample.Get(@"Menu/osu-logo-select"); + sampleBeat = audio.Sample.Get(@"Menu/osu-logo-heartbeat"); logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); @@ -266,6 +302,17 @@ namespace osu.Game.Screens.Menu } } + public void PlayIntro() + { + const double length = 3150; + const double fade = 200; + + logoHoverContainer.FadeOut().Delay(length).FadeIn(fade); + intro.Show(); + intro.Start(length); + intro.Delay(length + fade).FadeOut(); + } + protected override void Update() { base.Update(); @@ -290,9 +337,11 @@ namespace osu.Game.Screens.Menu } } + private bool interactive => Action != null && Alpha > 0.2f; + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - if (!Interactive) return false; + if (!interactive) return false; logoBounceContainer.ScaleTo(0.9f, 1000, Easing.Out); return true; @@ -306,21 +355,20 @@ namespace osu.Game.Screens.Menu protected override bool OnClick(InputState state) { - if (!Interactive) return false; + if (!interactive) return false; - sampleClick.Play(); + if (Action?.Invoke() ?? true) + sampleClick.Play(); flashLayer.ClearTransforms(); flashLayer.Alpha = 0.4f; flashLayer.FadeOut(1500, Easing.OutExpo); - - Action?.Invoke(); return true; } protected override bool OnHover(InputState state) { - if (!Interactive) return false; + if (!interactive) return false; logoHoverContainer.ScaleTo(1.1f, 500, Easing.OutElastic); return true; diff --git a/osu.Game/Screens/Multiplayer/DrawableRoom.cs b/osu.Game/Screens/Multiplayer/DrawableRoom.cs index d2f88224c2..0ba4aaa364 100644 --- a/osu.Game/Screens/Multiplayer/DrawableRoom.cs +++ b/osu.Game/Screens/Multiplayer/DrawableRoom.cs @@ -228,16 +228,16 @@ namespace osu.Game.Screens.Multiplayer if (value != null) { coverContainer.FadeIn(transition_duration); - coverContainer.Children = new[] + + + LoadComponentAsync(new BeatmapSetCover(value.BeatmapSet) { - new AsyncLoadWrapper(new BeatmapSetCover(value.BeatmapSet) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), - }) { RelativeSizeAxes = Axes.Both }, - }; + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), + }, + coverContainer.Add); beatmapTitle.Current = localisation.GetUnicodePreference(value.Metadata.TitleUnicode, value.Metadata.Title); beatmapDash.Text = @" - "; diff --git a/osu.Game/Screens/Multiplayer/Lobby.cs b/osu.Game/Screens/Multiplayer/Lobby.cs index d34da46ec3..b297835ca9 100644 --- a/osu.Game/Screens/Multiplayer/Lobby.cs +++ b/osu.Game/Screens/Multiplayer/Lobby.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace osu.Game.Screens.Multiplayer { - internal class Lobby : ScreenWhiteBox + public class Lobby : ScreenWhiteBox { protected override IEnumerable PossibleChildren => new[] { typeof(MatchCreate), diff --git a/osu.Game/Screens/Multiplayer/Match.cs b/osu.Game/Screens/Multiplayer/Match.cs index a0843bfcae..e50a7199a4 100644 --- a/osu.Game/Screens/Multiplayer/Match.cs +++ b/osu.Game/Screens/Multiplayer/Match.cs @@ -12,7 +12,7 @@ using osu.Framework.Graphics; namespace osu.Game.Screens.Multiplayer { - internal class Match : ScreenWhiteBox + public class Match : ScreenWhiteBox { protected override IEnumerable PossibleChildren => new[] { typeof(MatchSongSelect), diff --git a/osu.Game/Screens/Multiplayer/MatchCreate.cs b/osu.Game/Screens/Multiplayer/MatchCreate.cs index f28261fa7f..c232c38d08 100644 --- a/osu.Game/Screens/Multiplayer/MatchCreate.cs +++ b/osu.Game/Screens/Multiplayer/MatchCreate.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace osu.Game.Screens.Multiplayer { - internal class MatchCreate : ScreenWhiteBox + public class MatchCreate : ScreenWhiteBox { protected override IEnumerable PossibleChildren => new[] { typeof(Match) diff --git a/osu.Game/Screens/Multiplayer/RoomInspector.cs b/osu.Game/Screens/Multiplayer/RoomInspector.cs index 66ce51b428..8d7401500f 100644 --- a/osu.Game/Screens/Multiplayer/RoomInspector.cs +++ b/osu.Game/Screens/Multiplayer/RoomInspector.cs @@ -329,17 +329,16 @@ namespace osu.Game.Screens.Multiplayer if (value != null) { coverContainer.FadeIn(transition_duration); - coverContainer.Children = new[] + + LoadComponentAsync(new BeatmapSetCover(value.BeatmapSet) { - new AsyncLoadWrapper(new BeatmapSetCover(value.BeatmapSet) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), - }) { RelativeSizeAxes = Axes.Both }, - }; + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), + }, + coverContainer.Add); beatmapTitle.Current = localisation.GetUnicodePreference(value.Metadata.TitleUnicode, value.Metadata.Title); beatmapDash.Text = @" - "; diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index ae10d8828b..4a27c7f1ea 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -10,13 +10,17 @@ using osu.Game.Graphics.Containers; using OpenTK; using osu.Framework.Audio.Sample; using osu.Framework.Audio; +using osu.Framework.Graphics; using osu.Game.Rulesets; +using osu.Game.Screens.Menu; +using osu.Framework.Input; +using OpenTK.Input; namespace osu.Game.Screens { public abstract class OsuScreen : Screen { - internal BackgroundScreen Background { get; private set; } + public BackgroundScreen Background { get; private set; } /// /// Override to create a BackgroundMode for the current screen. @@ -24,17 +28,19 @@ namespace osu.Game.Screens /// protected virtual BackgroundScreen CreateBackground() => null; - internal virtual bool ShowOverlays => true; + public virtual bool ShowOverlays => true; protected new OsuGameBase Game => base.Game as OsuGameBase; - internal virtual bool HasLocalCursorDisplayed => false; + public virtual bool HasLocalCursorDisplayed => false; + + private OsuLogo logo; /// /// Whether the beatmap or ruleset should be allowed to be changed by the user or game. /// Used to mark exclusive areas where this is strongly prohibited, like gameplay. /// - internal virtual bool AllowBeatmapRulesetChange => true; + public virtual bool AllowBeatmapRulesetChange => true; protected readonly Bindable Beatmap = new Bindable(); @@ -66,15 +72,36 @@ namespace osu.Game.Screens if (osuGame != null) Ruleset.BindTo(osuGame.Ruleset); - sampleExit = audio.Sample.Get(@"UI/melodic-1"); + sampleExit = audio.Sample.Get(@"UI/screen-back"); + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Repeat || !IsCurrentScreen) return false; + + switch (args.Key) + { + case Key.Escape: + Exit(); + return true; + } + + return base.OnKeyDown(state, args); } protected override void OnResuming(Screen last) { base.OnResuming(last); + logo.AppendAnimatingAction(() => LogoArriving(logo, true), true); sampleExit?.Play(); } + protected override void OnSuspending(Screen next) + { + base.OnSuspending(next); + onSuspendingLogo(); + } + protected override void OnEntering(Screen last) { OsuScreen lastOsu = last as OsuScreen; @@ -106,11 +133,19 @@ namespace osu.Game.Screens }); } + if ((logo = lastOsu?.logo) == null) + LoadComponentAsync(logo = new OsuLogo { Alpha = 0 }, AddInternal); + + logo.AppendAnimatingAction(() => LogoArriving(logo, false), true); + base.OnEntering(last); } protected override bool OnExiting(Screen next) { + if (ValidForResume && logo != null) + onExitingLogo(); + OsuScreen nextOsu = next as OsuScreen; if (Background != null && !Background.Equals(nextOsu?.Background)) @@ -128,5 +163,43 @@ namespace osu.Game.Screens Beatmap.UnbindAll(); return false; } + + /// + /// Fired when this screen was entered or resumed and the logo state is required to be adjusted. + /// + protected virtual void LogoArriving(OsuLogo logo, bool resuming) + { + logo.Action = null; + logo.FadeOut(300, Easing.OutQuint); + logo.Anchor = Anchor.TopLeft; + logo.Origin = Anchor.Centre; + logo.RelativePositionAxes = Axes.None; + logo.Triangles = true; + logo.Ripple = true; + } + + private void onExitingLogo() + { + logo.AppendAnimatingAction(() => { LogoExiting(logo); }, false); + } + + /// + /// Fired when this screen was exited to add any outwards transition to the logo. + /// + protected virtual void LogoExiting(OsuLogo logo) + { + } + + private void onSuspendingLogo() + { + logo.AppendAnimatingAction(() => { LogoSuspending(logo); }, false); + } + + /// + /// Fired when this screen was suspended to add any outwards transition to the logo. + /// + protected virtual void LogoSuspending(OsuLogo logo) + { + } } } diff --git a/osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs b/osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs new file mode 100644 index 0000000000..0b775d5c35 --- /dev/null +++ b/osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs @@ -0,0 +1,103 @@ +// 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 OpenTK; +using osu.Game.Graphics.Containers; +using osu.Game.Beatmaps.Timing; + +namespace osu.Game.Screens.Play.BreaksOverlay +{ + public class ArrowsOverlay : VisibilityContainer + { + private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; + + private const int glow_icon_size = 60; + private const int glow_icon_blur_sigma = 10; + private const float glow_icon_final_offset = 0.22f; + private const float glow_icon_offscreen_offset = 0.6f; + + private const int blurred_icon_blur_sigma = 20; + private const int blurred_icon_size = 130; + private const float blurred_icon_final_offset = 0.35f; + private const float blurred_icon_offscreen_offset = 0.7f; + + private readonly GlowIcon leftGlowIcon; + private readonly GlowIcon rightGlowIcon; + + private readonly BlurredIcon leftBlurredIcon; + private readonly BlurredIcon rightBlurredIcon; + + public ArrowsOverlay() + { + RelativeSizeAxes = Axes.Both; + Children = new Drawable[] + { + leftGlowIcon = new GlowIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + X = -glow_icon_offscreen_offset, + Icon = Graphics.FontAwesome.fa_chevron_right, + BlurSigma = new Vector2(glow_icon_blur_sigma), + Size = new Vector2(glow_icon_size), + }, + rightGlowIcon = new GlowIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + X = glow_icon_offscreen_offset, + Icon = Graphics.FontAwesome.fa_chevron_left, + BlurSigma = new Vector2(glow_icon_blur_sigma), + Size = new Vector2(glow_icon_size), + }, + new ParallaxContainer + { + ParallaxAmount = -0.02f, + Children = new Drawable[] + { + leftBlurredIcon = new BlurredIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Alpha = 0.7f, + X = -blurred_icon_offscreen_offset, + Icon = Graphics.FontAwesome.fa_chevron_right, + BlurSigma = new Vector2(blurred_icon_blur_sigma), + Size = new Vector2(blurred_icon_size), + }, + rightBlurredIcon = new BlurredIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Alpha = 0.7f, + X = blurred_icon_offscreen_offset, + Icon = Graphics.FontAwesome.fa_chevron_left, + BlurSigma = new Vector2(blurred_icon_blur_sigma), + Size = new Vector2(blurred_icon_size), + }, + } + } + }; + } + + protected override void PopIn() + { + leftGlowIcon.MoveToX(-glow_icon_final_offset, fade_duration, Easing.OutQuint); + rightGlowIcon.MoveToX(glow_icon_final_offset, fade_duration, Easing.OutQuint); + + leftBlurredIcon.MoveToX(-blurred_icon_final_offset, fade_duration, Easing.OutQuint); + rightBlurredIcon.MoveToX(blurred_icon_final_offset, fade_duration, Easing.OutQuint); + } + + protected override void PopOut() + { + leftGlowIcon.MoveToX(-glow_icon_offscreen_offset, fade_duration, Easing.OutQuint); + rightGlowIcon.MoveToX(glow_icon_offscreen_offset, fade_duration, Easing.OutQuint); + + leftBlurredIcon.MoveToX(-blurred_icon_offscreen_offset, fade_duration, Easing.OutQuint); + rightBlurredIcon.MoveToX(blurred_icon_offscreen_offset, fade_duration, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs b/osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs new file mode 100644 index 0000000000..f16e7c7b96 --- /dev/null +++ b/osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Framework.Allocation; + +namespace osu.Game.Screens.Play.BreaksOverlay +{ + public class BlurredIcon : BufferedContainer + { + private readonly SpriteIcon icon; + + public FontAwesome Icon + { + set { icon.Icon = value; } + get { return icon.Icon; } + } + + public override Vector2 Size + { + set + { + icon.Size = value; + base.Size = value + BlurSigma * 2.5f; + ForceRedraw(); + } + get { return base.Size; } + } + + public BlurredIcon() + { + RelativePositionAxes = Axes.X; + CacheDrawnFrameBuffer = true; + Child = icon = new SpriteIcon + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Shadow = false, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.BlueLighter; + } + } +} diff --git a/osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs b/osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs new file mode 100644 index 0000000000..7ef1ef8d8a --- /dev/null +++ b/osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs @@ -0,0 +1,157 @@ +// 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.Shapes; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Scoring; +using System.Collections.Generic; +using osu.Framework.Graphics.UserInterface; + +namespace osu.Game.Screens.Play.BreaksOverlay +{ + public class BreakOverlay : Container + { + private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; + private const float remaining_time_container_max_size = 0.3f; + private const int vertical_margin = 25; + + private List breaks; + public List Breaks + { + set + { + breaks = value; + initializeBreaks(); + } + get + { + return breaks; + } + } + + public override bool RemoveCompletedTransforms => false; + + private readonly bool letterboxing; + private readonly LetterboxOverlay letterboxOverlay; + private readonly Container remainingTimeAdjustmentBox; + private readonly Container remainingTimeBox; + private readonly RemainingTimeCounter remainingTimeCounter; + private readonly InfoContainer info; + private readonly ArrowsOverlay arrowsOverlay; + + public BreakOverlay(bool letterboxing) + { + this.letterboxing = letterboxing; + + RelativeSizeAxes = Axes.Both; + Children = new Drawable[] + { + letterboxOverlay = new LetterboxOverlay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + remainingTimeAdjustmentBox = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Width = 0, + Child = remainingTimeBox = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 8, + CornerRadius = 4, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both } + } + }, + remainingTimeCounter = new RemainingTimeCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding { Bottom = vertical_margin }, + }, + info = new InfoContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = vertical_margin }, + }, + arrowsOverlay = new ArrowsOverlay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + private void initializeBreaks() + { + FinishTransforms(true); + Scheduler.CancelDelayedTasks(); + + if (breaks == null) + return; + + foreach (var b in breaks) + { + if (!b.HasEffect) + continue; + + using (BeginAbsoluteSequence(b.StartTime, true)) + { + remainingTimeAdjustmentBox + .ResizeWidthTo(remaining_time_container_max_size, fade_duration, Easing.OutQuint) + .Delay(b.Duration - fade_duration) + .ResizeWidthTo(0); + + remainingTimeBox + .ResizeWidthTo(0, b.Duration - fade_duration) + .Then() + .ResizeWidthTo(1); + + remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); + } + + using (BeginAbsoluteSequence(b.StartTime)) + { + Schedule(showBreak); + using (BeginDelayedSequence(b.Duration - fade_duration)) + Schedule(hideBreak); + } + } + } + + private void showBreak() + { + if (letterboxing) + letterboxOverlay.Show(); + + remainingTimeCounter.Show(); + info.Show(); + arrowsOverlay.Show(); + } + + private void hideBreak() + { + if (letterboxing) + letterboxOverlay.Hide(); + + remainingTimeCounter.Hide(); + info.Hide(); + arrowsOverlay.Hide(); + } + + public void BindProcessor(ScoreProcessor processor) + { + info.AccuracyDisplay.Current.BindTo(processor.Accuracy); + info.GradeDisplay.Current.BindTo(processor.Rank); + } + } +} diff --git a/osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs b/osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs new file mode 100644 index 0000000000..b27eef632c --- /dev/null +++ b/osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using OpenTK; +using osu.Framework.Allocation; + +namespace osu.Game.Screens.Play.BreaksOverlay +{ + public class GlowIcon : Container + { + private readonly SpriteIcon spriteIcon; + private readonly BlurredIcon blurredIcon; + + public override Vector2 Size + { + set + { + blurredIcon.Size = spriteIcon.Size = value; + blurredIcon.ForceRedraw(); + } + get { return base.Size; } + } + + public Vector2 BlurSigma + { + set { blurredIcon.BlurSigma = value; } + get { return blurredIcon.BlurSigma; } + } + + public FontAwesome Icon + { + set { spriteIcon.Icon = blurredIcon.Icon = value; } + get { return spriteIcon.Icon; } + } + + public GlowIcon() + { + RelativePositionAxes = Axes.X; + AutoSizeAxes = Axes.Both; + Children = new Drawable[] + { + blurredIcon = new BlurredIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + spriteIcon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Shadow = false, + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + blurredIcon.Colour = colours.Blue; + } + } +} diff --git a/osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs b/osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs new file mode 100644 index 0000000000..1bf9b26cc9 --- /dev/null +++ b/osu.Game/Screens/Play/BreaksOverlay/InfoContainer.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 OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Scoring; +using osu.Game.Beatmaps.Timing; + +namespace osu.Game.Screens.Play.BreaksOverlay +{ + public class InfoContainer : VisibilityContainer + { + private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; + + public PercentageInfoLine AccuracyDisplay; + public InfoLine RankDisplay; + public InfoLine GradeDisplay; + + public InfoContainer() + { + AutoSizeAxes = Axes.Both; + Child = new FillFlowContainer + { + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "current progress".ToUpper(), + TextSize = 15, + Font = "Exo2.0-Black", + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + AccuracyDisplay = new PercentageInfoLine("Accuracy"), + RankDisplay = new InfoLine("Rank"), + GradeDisplay = new InfoLine("Grade"), + }, + } + }, + }; + } + + protected override void PopIn() => this.FadeIn(fade_duration); + protected override void PopOut() => this.FadeOut(fade_duration); + } +} diff --git a/osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs b/osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs new file mode 100644 index 0000000000..751523d68c --- /dev/null +++ b/osu.Game/Screens/Play/BreaksOverlay/InfoLine.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.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Play.BreaksOverlay +{ + public class InfoLine : Container + where T : struct + { + private const int margin = 2; + + public Bindable Current = new Bindable(); + + private readonly OsuSpriteText text; + private readonly OsuSpriteText valueText; + + private readonly string prefix; + + public InfoLine(string name, string prefix = @"") + { + this.prefix = prefix; + + AutoSizeAxes = Axes.Y; + Children = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Text = name, + TextSize = 17, + Margin = new MarginPadding { Right = margin } + }, + valueText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Text = prefix + @"-", + TextSize = 17, + Font = "Exo2.0-Bold", + Margin = new MarginPadding { Left = margin } + } + }; + + Current.ValueChanged += currentValueChanged; + } + + private void currentValueChanged(T newValue) + { + var newText = prefix + Format(newValue); + + if (valueText.Text == newText) + return; + + valueText.Text = newText; + } + + protected virtual string Format(T count) => count.ToString(); + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + text.Colour = colours.Yellow; + valueText.Colour = colours.YellowLight; + } + } + + public class PercentageInfoLine : InfoLine + { + public PercentageInfoLine(string name, string prefix = "") : base(name, prefix) + { + } + + protected override string Format(double count) => $@"{count:P2}"; + } +} diff --git a/osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs b/osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs new file mode 100644 index 0000000000..4733a5482b --- /dev/null +++ b/osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.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 OpenTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.Timing; + +namespace osu.Game.Screens.Play.BreaksOverlay +{ + public class LetterboxOverlay : VisibilityContainer + { + private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; + private const int height = 350; + + private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); + + public LetterboxOverlay() + { + RelativeSizeAxes = Axes.Both; + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + RelativeSizeAxes = Axes.X, + Height = height, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black), + } + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = height, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black), + } + } + }; + } + + protected override void PopIn() => this.FadeIn(fade_duration); + protected override void PopOut() => this.FadeOut(fade_duration); + } +} diff --git a/osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs b/osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs new file mode 100644 index 0000000000..0df7bb97e0 --- /dev/null +++ b/osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.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 osu.Game.Graphics.Sprites; +using osu.Framework.Graphics; +using System; +using osu.Game.Beatmaps.Timing; +using osu.Framework.Graphics.UserInterface; + +namespace osu.Game.Screens.Play.BreaksOverlay +{ + public class RemainingTimeCounter : Counter + { + private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; + + private readonly OsuSpriteText counter; + + public RemainingTimeCounter() + { + AutoSizeAxes = Axes.Both; + InternalChild = counter = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + TextSize = 33, + Font = "Venera", + }; + + Alpha = 0; + } + + protected override void OnCountChanged(double count) => counter.Text = ((int)Math.Ceiling(count / 1000)).ToString(); + + public override void Show() => this.FadeIn(fade_duration); + public override void Hide() => this.FadeOut(fade_duration); + } +} diff --git a/osu.Game/Screens/Play/FailOverlay.cs b/osu.Game/Screens/Play/FailOverlay.cs index 3e31da2348..09f2e15c57 100644 --- a/osu.Game/Screens/Play/FailOverlay.cs +++ b/osu.Game/Screens/Play/FailOverlay.cs @@ -10,7 +10,7 @@ using System.Linq; namespace osu.Game.Screens.Play { - public class FailOverlay : MenuOverlay + public class FailOverlay : GameplayMenuOverlay { public override string Header => "failed"; public override string Description => "you're dead, try again?"; @@ -18,15 +18,15 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(OsuColour colours) { - AddButton("Retry", colours.YellowDark, OnRetry); - AddButton("Quit", new Color4(170, 27, 39, 255), OnQuit); + AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); + AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { if (!args.Repeat && args.Key == Key.Escape) { - Buttons.Children.Last().TriggerOnClick(); + InternalButtons.Children.Last().TriggerOnClick(); return true; } diff --git a/osu.Game/Screens/Play/MenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs similarity index 56% rename from osu.Game/Screens/Play/MenuOverlay.cs rename to osu.Game/Screens/Play/GameplayMenuOverlay.cs index 0a8e172e57..6969cd915b 100644 --- a/osu.Game/Screens/Play/MenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -13,10 +13,12 @@ using osu.Game.Graphics; using osu.Framework.Allocation; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Shapes; +using OpenTK.Input; +using System.Collections.Generic; namespace osu.Game.Screens.Play { - public abstract class MenuOverlay : OverlayContainer, IRequireHighFrequencyMousePosition + public abstract class GameplayMenuOverlay : OverlayContainer, IRequireHighFrequencyMousePosition { private const int transition_duration = 200; private const int button_height = 70; @@ -24,81 +26,24 @@ namespace osu.Game.Screens.Play protected override bool BlockPassThroughKeyboard => true; + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; + public Action OnRetry; public Action OnQuit; public abstract string Header { get; } public abstract string Description { get; } - protected FillFlowContainer Buttons; - - public int Retries - { - set - { - if (retryCounterContainer != null) - { - // "You've retried 1,065 times in this session" - // "You've retried 1 time in this session" - - retryCounterContainer.Children = new Drawable[] - { - new OsuSpriteText - { - Text = "You've retried ", - Shadow = true, - ShadowColour = new Color4(0, 0, 0, 0.25f), - TextSize = 18 - }, - new OsuSpriteText - { - Text = $"{value:n0}", - Font = @"Exo2.0-Bold", - Shadow = true, - ShadowColour = new Color4(0, 0, 0, 0.25f), - TextSize = 18 - }, - new OsuSpriteText - { - Text = $" time{(value == 1 ? "" : "s")} in this session", - Shadow = true, - ShadowColour = new Color4(0, 0, 0, 0.25f), - TextSize = 18 - } - }; - } - } - } + protected internal FillFlowContainer InternalButtons; + public IReadOnlyList Buttons => InternalButtons; private FillFlowContainer retryCounterContainer; - public override bool HandleInput => State == Visibility.Visible; - - protected override void PopIn() => this.FadeIn(transition_duration, Easing.In); - protected override void PopOut() => this.FadeOut(transition_duration, Easing.In); - - // Don't let mouse down events through the overlay or people can click circles while paused. - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => true; - - protected override bool OnMouseMove(InputState state) => true; - - protected void AddButton(string text, Color4 colour, Action action) + protected GameplayMenuOverlay() { - Buttons.Add(new Button - { - Text = text, - ButtonColour = colour, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Height = button_height, - Action = delegate - { - action?.Invoke(); - Hide(); - } - }); + RelativeSizeAxes = Axes.Both; + + StateChanged += s => selectionIndex = -1; } [BackgroundDependencyLoader] @@ -154,7 +99,7 @@ namespace osu.Game.Screens.Play } } }, - Buttons = new FillFlowContainer + InternalButtons = new FillFlowContainer { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, @@ -179,16 +124,153 @@ namespace osu.Game.Screens.Play }, }; - Retries = 0; + updateRetryCount(); } - protected MenuOverlay() + private int retries; + + public int Retries { - RelativeSizeAxes = Axes.Both; + set + { + if (value == retries) + return; + + retries = value; + if (retryCounterContainer != null) + updateRetryCount(); + } } - public class Button : DialogButton + public override bool HandleInput => State == Visibility.Visible; + + protected override void PopIn() => this.FadeIn(transition_duration, Easing.In); + protected override void PopOut() => this.FadeOut(transition_duration, Easing.In); + + // Don't let mouse down events through the overlay or people can click circles while paused. + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => true; + + protected override bool OnMouseMove(InputState state) => true; + + protected void AddButton(string text, Color4 colour, Action action) { + var button = new Button + { + Text = text, + ButtonColour = colour, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Height = button_height, + Action = delegate + { + action?.Invoke(); + Hide(); + } + }; + + button.Selected.ValueChanged += s => buttonSelectionChanged(button, s); + + InternalButtons.Add(button); + } + + private int _selectionIndex = -1; + + private int selectionIndex + { + get { return _selectionIndex; } + set + { + if (_selectionIndex == value) + return; + + // Deselect the previously-selected button + if (_selectionIndex != -1) + InternalButtons[_selectionIndex].Selected.Value = false; + + _selectionIndex = value; + + // Select the newly-selected button + if (_selectionIndex != -1) + InternalButtons[_selectionIndex].Selected.Value = true; + } + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (!args.Repeat) + { + switch (args.Key) + { + case Key.Up: + if (selectionIndex == -1 || selectionIndex == 0) + selectionIndex = InternalButtons.Count - 1; + else + selectionIndex--; + return true; + case Key.Down: + if (selectionIndex == -1 || selectionIndex == InternalButtons.Count - 1) + selectionIndex = 0; + else + selectionIndex++; + return true; + } + } + + return base.OnKeyDown(state, args); + } + + private void buttonSelectionChanged(DialogButton button, bool isSelected) + { + if (!isSelected) + selectionIndex = -1; + else + selectionIndex = InternalButtons.IndexOf(button); + } + + private void updateRetryCount() + { + // "You've retried 1,065 times in this session" + // "You've retried 1 time in this session" + + retryCounterContainer.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "You've retried ", + Shadow = true, + ShadowColour = new Color4(0, 0, 0, 0.25f), + TextSize = 18 + }, + new OsuSpriteText + { + Text = $"{retries:n0}", + Font = @"Exo2.0-Bold", + Shadow = true, + ShadowColour = new Color4(0, 0, 0, 0.25f), + TextSize = 18 + }, + new OsuSpriteText + { + Text = $" time{(retries == 1 ? "" : "s")} in this session", + Shadow = true, + ShadowColour = new Color4(0, 0, 0, 0.25f), + TextSize = 18 + } + }; + } + + private class Button : DialogButton + { + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Repeat || args.Key != Key.Enter || !Selected) + return false; + + OnClick(state); + return true; + } } } } diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 18a3096d7c..fd0e71ece8 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -55,11 +55,7 @@ namespace osu.Game.Screens.Play.HUD iconsContainer.Clear(); foreach (Mod mod in mods) { - iconsContainer.Add(new ModIcon(mod) - { - AutoSizeAxes = Axes.Both, - Scale = new Vector2(0.6f), - }); + iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); } if (IsLoaded) diff --git a/osu.Game/Screens/Play/HUD/ReplaySettingsOverlay.cs b/osu.Game/Screens/Play/HUD/ReplaySettingsOverlay.cs new file mode 100644 index 0000000000..c07eebdef8 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ReplaySettingsOverlay.cs @@ -0,0 +1,66 @@ +// 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.Game.Screens.Play.ReplaySettings; +using OpenTK; +using osu.Framework.Input; +using OpenTK.Input; + +namespace osu.Game.Screens.Play.HUD +{ + public class ReplaySettingsOverlay : VisibilityContainer + { + private const int fade_duration = 200; + + public bool ReplayLoaded; + + public readonly PlaybackSettings PlaybackSettings; + //public readonly CollectionSettings CollectionSettings; + //public readonly DiscussionSettings DiscussionSettings; + + public ReplaySettingsOverlay() + { + AlwaysPresent = true; + RelativeSizeAxes = Axes.Both; + + Child = new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding { Top = 100, Right = 10 }, + Children = new[] + { + //CollectionSettings = new CollectionSettings(), + //DiscussionSettings = new DiscussionSettings(), + PlaybackSettings = new PlaybackSettings(), + } + }; + + State = Visibility.Visible; + } + + protected override void PopIn() => this.FadeIn(fade_duration); + protected override void PopOut() => this.FadeOut(fade_duration); + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Repeat) return false; + + if (state.Keyboard.ControlPressed) + { + if (args.Key == Key.H && ReplayLoaded) + { + ToggleVisibility(); + return true; + } + } + + return base.OnKeyDown(state, args); + } + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index ddcf031bd4..f4b5efe1e5 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -32,6 +32,7 @@ namespace osu.Game.Screens.Play public readonly HealthDisplay HealthDisplay; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; + public readonly ReplaySettingsOverlay ReplaySettingsOverlay; private Bindable showHud; private bool replayLoaded; @@ -55,7 +56,7 @@ namespace osu.Game.Screens.Play HealthDisplay = CreateHealthDisplay(), Progress = CreateProgress(), ModDisplay = CreateModsContainer(), - //ReplaySettingsOverlay = CreateReplaySettingsOverlay(), + ReplaySettingsOverlay = CreateReplaySettingsOverlay(), } }); } @@ -96,9 +97,14 @@ namespace osu.Game.Screens.Play replayLoaded = rulesetContainer.HasReplayLoaded; + ReplaySettingsOverlay.ReplayLoaded = replayLoaded; + // in the case a replay isn't loaded, we want some elements to only appear briefly. if (!replayLoaded) + { + ReplaySettingsOverlay.Hide(); ModDisplay.Delay(2000).FadeOut(200); + } } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) @@ -176,12 +182,7 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 20, Right = 10 }, }; - //protected virtual ReplaySettingsOverlay CreateReplaySettingsOverlay() => new ReplaySettingsOverlay - //{ - // Anchor = Anchor.TopRight, - // Origin = Anchor.TopRight, - // Margin = new MarginPadding { Top = 100, Right = 10 }, - //}; + protected virtual ReplaySettingsOverlay CreateReplaySettingsOverlay() => new ReplaySettingsOverlay(); public virtual void BindProcessor(ScoreProcessor processor) { diff --git a/osu.Game/Screens/Play/KeyCounterCollection.cs b/osu.Game/Screens/Play/KeyCounterCollection.cs index d21be71785..2f077f5c0a 100644 --- a/osu.Game/Screens/Play/KeyCounterCollection.cs +++ b/osu.Game/Screens/Play/KeyCounterCollection.cs @@ -141,8 +141,6 @@ namespace osu.Game.Screens.Play public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; - public override bool HandleInput => true; - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => Target.Children.Any(c => c.TriggerOnKeyDown(state, args)); protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => Target.Children.Any(c => c.TriggerOnKeyUp(state, args)); diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs index eed5cd1c20..3bd28511c7 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PauseContainer.cs @@ -22,8 +22,6 @@ namespace osu.Game.Screens.Play { public bool IsPaused { get; private set; } - public bool AllowExit => IsPaused && pauseOverlay.Alpha == 1; - public Func CheckCanPause; private const double pause_cooldown = 1000; @@ -121,7 +119,7 @@ namespace osu.Game.Screens.Play base.Update(); } - public class PauseOverlay : MenuOverlay + public class PauseOverlay : GameplayMenuOverlay { public Action OnResume; @@ -132,7 +130,7 @@ namespace osu.Game.Screens.Play { if (!args.Repeat && args.Key == Key.Escape) { - Buttons.Children.First().TriggerOnClick(); + InternalButtons.Children.First().TriggerOnClick(); return true; } @@ -142,9 +140,9 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(OsuColour colours) { - AddButton("Continue", colours.Green, OnResume); - AddButton("Retry", colours.YellowDark, OnRetry); - AddButton("Quit", new Color4(170, 27, 39, 255), OnQuit); + AddButton("Continue", colours.Green, () => OnResume?.Invoke()); + AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); + AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e120c7f193..340fc39d52 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI; using osu.Game.Screens.Backgrounds; using System; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Threading; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -24,6 +25,7 @@ using osu.Game.Screens.Ranking; using osu.Framework.Audio.Sample; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Screens.Play.BreaksOverlay; using osu.Game.Storyboards.Drawables; using OpenTK.Graphics; @@ -33,16 +35,18 @@ namespace osu.Game.Screens.Play { protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); - internal override bool ShowOverlays => false; + public override bool ShowOverlays => false; - internal override bool HasLocalCursorDisplayed => !pauseContainer.IsPaused && !HasFailed && RulesetContainer.ProvidingUserCursor; + public override bool HasLocalCursorDisplayed => !pauseContainer.IsPaused && !HasFailed && RulesetContainer.ProvidingUserCursor; public Action RestartRequested; - internal override bool AllowBeatmapRulesetChange => false; + public override bool AllowBeatmapRulesetChange => false; public bool HasFailed { get; private set; } + public bool AllowPause { get; set; } = true; + public int RestartCount; private IAdjustableClock adjustableSourceClock; @@ -69,6 +73,7 @@ namespace osu.Game.Screens.Play #endregion + private BreakOverlay breakOverlay; private Container storyboardContainer; private DrawableStoryboard storyboard; @@ -140,16 +145,6 @@ namespace osu.Game.Screens.Play userAudioOffset.ValueChanged += v => offsetClock.Offset = v; userAudioOffset.TriggerChange(); - Schedule(() => - { - adjustableSourceClock.Reset(); - - foreach (var mod in working.Mods.Value.OfType()) - mod.ApplyToClock(adjustableSourceClock); - - decoupledClock.ChangeSource(adjustableSourceClock); - }); - Children = new Drawable[] { storyboardContainer = new Container @@ -164,9 +159,9 @@ namespace osu.Game.Screens.Play FramedClock = offsetClock, OnRetry = Restart, OnQuit = Exit, - CheckCanPause = () => ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded, - Retries = RestartCount, + CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded, OnPause = () => { + pauseContainer.Retries = RestartCount; hudOverlay.KeyCounter.IsCounting = pauseContainer.IsPaused; }, OnResume = () => { @@ -179,16 +174,20 @@ namespace osu.Game.Screens.Play { RelativeSizeAxes = Axes.Both, Clock = offsetClock, - Children = new Drawable[] - { - RulesetContainer, - } + Child = RulesetContainer, }, hudOverlay = new HUDOverlay { Anchor = Anchor.Centre, Origin = Anchor.Centre }, + breakOverlay = new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Clock = decoupledClock, + Breaks = beatmap.Breaks + }, } }, failOverlay = new FailOverlay @@ -222,19 +221,38 @@ namespace osu.Game.Screens.Play hudOverlay.ModDisplay.Current.BindTo(working.Mods); + breakOverlay.BindProcessor(scoreProcessor); + + hudOverlay.ReplaySettingsOverlay.PlaybackSettings.AdjustableClock = adjustableSourceClock; + // Bind ScoreProcessor to ourselves scoreProcessor.AllJudged += onCompletion; scoreProcessor.Failed += onFail; + + foreach (var mod in Beatmap.Value.Mods.Value.OfType()) + mod.ApplyToScoreProcessor(scoreProcessor); + } + + private void applyRateFromMods() + { + if (adjustableSourceClock == null) return; + + adjustableSourceClock.Rate = 1; + foreach (var mod in Beatmap.Value.Mods.Value.OfType()) + mod.ApplyToClock(adjustableSourceClock); } private void initializeStoryboard(bool asyncLoad) { - var beatmap = Beatmap.Value.Beatmap; + var beatmap = Beatmap.Value; storyboard = beatmap.Storyboard.CreateDrawable(Beatmap.Value); storyboard.Masking = true; - storyboardContainer.Add(asyncLoad ? new AsyncLoadWrapper(storyboard) { RelativeSizeAxes = Axes.Both } : (Drawable)storyboard); + if (asyncLoad) + LoadComponentAsync(storyboard, storyboardContainer.Add); + else + storyboardContainer.Add(storyboard); } public void Restart() @@ -291,7 +309,7 @@ namespace osu.Game.Screens.Play if (!loadedSuccessfully) return; - (Background as BackgroundScreenBeatmap)?.BlurTo(Vector2.Zero, 1500, Easing.OutQuint); + (Background as BackgroundScreenBeatmap)?.BlurTo(Vector2.Zero, 1000, Easing.OutQuint); dimLevel.ValueChanged += dimLevel_ValueChanged; showStoryboard.ValueChanged += showStoryboard_ValueChanged; @@ -304,10 +322,21 @@ namespace osu.Game.Screens.Play .Delay(250) .FadeIn(250); - this.Delay(750).Schedule(() => + Task.Run(() => { - if (!pauseContainer.IsPaused) - decoupledClock.Start(); + adjustableSourceClock.Reset(); + + Schedule(() => + { + decoupledClock.ChangeSource(adjustableSourceClock); + applyRateFromMods(); + + this.Delay(750).Schedule(() => + { + if (!pauseContainer.IsPaused) + decoupledClock.Start(); + }); + }); }); pauseContainer.Alpha = 0; @@ -322,8 +351,11 @@ namespace osu.Game.Screens.Play protected override bool OnExiting(Screen next) { - if (HasFailed || !ValidForResume || pauseContainer?.AllowExit != false || RulesetContainer?.HasReplayLoaded != false) + if (!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) { + // In the case of replays, we may have changed the playback rate. + applyRateFromMods(); + fadeOut(); return base.OnExiting(next); } @@ -350,7 +382,7 @@ namespace osu.Game.Screens.Play initializeStoryboard(true); var beatmap = Beatmap.Value; - var storyboardVisible = showStoryboard && beatmap.Beatmap.Storyboard.HasDrawable; + var storyboardVisible = showStoryboard && beatmap.Storyboard.HasDrawable; storyboardContainer.FadeColour(new Color4(opacity, opacity, opacity, 1), 800); storyboardContainer.FadeTo(storyboardVisible && opacity > 0 ? 1 : 0); diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 9366797f47..de67bef004 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -10,9 +10,9 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Backgrounds; -using osu.Game.Screens.Menu; using OpenTK; using osu.Framework.Localisation; +using osu.Game.Screens.Menu; namespace osu.Game.Screens.Play { @@ -20,13 +20,12 @@ namespace osu.Game.Screens.Play { private Player player; - private readonly OsuLogo logo; private BeatmapMetadataDisplay info; private bool showOverlays = true; - internal override bool ShowOverlays => showOverlays; + public override bool ShowOverlays => showOverlays; - internal override bool AllowBeatmapRulesetChange => false; + public override bool AllowBeatmapRulesetChange => false; protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); @@ -39,15 +38,6 @@ namespace osu.Game.Screens.Play showOverlays = false; ValidForResume = true; }; - - Children = new Drawable[] - { - logo = new OsuLogo - { - Scale = new Vector2(0.15f), - Interactive = false, - }, - }; } [BackgroundDependencyLoader] @@ -101,11 +91,23 @@ namespace osu.Game.Screens.Play contentIn(); - logo.Delay(500).MoveToOffset(new Vector2(0, -180), 500, Easing.InOutExpo); info.Delay(750).FadeIn(500); this.Delay(2150).Schedule(pushWhenLoaded); } + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + logo.RelativePositionAxes = Axes.Both; + + logo.ScaleTo(new Vector2(0.15f), 300, Easing.In); + logo.MoveTo(new Vector2(0.5f), 300, Easing.In); + logo.FadeIn(350); + + logo.Delay(resuming ? 0 : 500).MoveToOffset(new Vector2(0, -0.24f), 500, Easing.InOutExpo); + } + private void pushWhenLoaded() { if (player.LoadState != LoadState.Ready) @@ -248,7 +250,7 @@ namespace osu.Game.Screens.Play Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, }, - new MetadataLine("Mapper", metadata.Author) + new MetadataLine("Mapper", metadata.Author.Username) { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, diff --git a/osu.Game/Screens/Play/ReplaySettings/PlaybackSettings.cs b/osu.Game/Screens/Play/ReplaySettings/PlaybackSettings.cs index ea958d05f9..3109552532 100644 --- a/osu.Game/Screens/Play/ReplaySettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/ReplaySettings/PlaybackSettings.cs @@ -1,27 +1,76 @@ // 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.Game.Configuration; +using osu.Framework.Timing; +using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; namespace osu.Game.Screens.Play.ReplaySettings { public class PlaybackSettings : ReplayGroup { + private const int padding = 10; + protected override string Title => @"playback"; - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + public IAdjustableClock AdjustableClock { set; get; } + + private readonly ReplaySliderBar sliderbar; + + public PlaybackSettings() { + OsuSpriteText multiplierText; + Children = new Drawable[] { - new ReplaySliderBar + new Container { - LabelText = "Playback speed", - Bindable = config.GetBindable(OsuSetting.PlaybackSpeed) + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = padding }, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = "Playback speed", + }, + multiplierText = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Text = "1x", + Font = @"Exo2.0-Bold", + } + }, + }, + sliderbar = new ReplaySliderBar + { + Bindable = new BindableDouble(1) + { + Default = 1, + MinValue = 0.5, + MaxValue = 2, + Precision = 0.01, + }, } }; + + sliderbar.Bindable.ValueChanged += rateMultiplier => multiplierText.Text = $"{rateMultiplier}x"; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (AdjustableClock == null) + return; + + var clockRate = AdjustableClock.Rate; + sliderbar.Bindable.ValueChanged += rateMultiplier => AdjustableClock.Rate = clockRate * rateMultiplier; } } } diff --git a/osu.Game/Screens/Play/ReplaySettingsOverlay.cs b/osu.Game/Screens/Play/ReplaySettingsOverlay.cs deleted file mode 100644 index 415f70005d..0000000000 --- a/osu.Game/Screens/Play/ReplaySettingsOverlay.cs +++ /dev/null @@ -1,24 +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; -using osu.Framework.Graphics.Containers; -using osu.Game.Screens.Play.ReplaySettings; -using OpenTK; - -namespace osu.Game.Screens.Play -{ - public class ReplaySettingsOverlay : FillFlowContainer - { - public ReplaySettingsOverlay() - { - Direction = FillDirection.Vertical; - AutoSizeAxes = Axes.Both; - Spacing = new Vector2(0, 20); - - Add(new CollectionSettings()); - Add(new DiscussionSettings()); - Add(new PlaybackSettings()); - } - } -} diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs index 60ad484673..8e27cb235c 100644 --- a/osu.Game/Screens/Ranking/Results.cs +++ b/osu.Game/Screens/Ranking/Results.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking private ResultModeTabControl modeChangeButtons; - internal override bool AllowBeatmapRulesetChange => false; + public override bool AllowBeatmapRulesetChange => false; private Container currentPage; diff --git a/osu.Game/Screens/Ranking/ResultsPage.cs b/osu.Game/Screens/Ranking/ResultsPage.cs index 7f381ebf99..037d5ada09 100644 --- a/osu.Game/Screens/Ranking/ResultsPage.cs +++ b/osu.Game/Screens/Ranking/ResultsPage.cs @@ -14,7 +14,7 @@ using OpenTK.Graphics; namespace osu.Game.Screens.Ranking { - internal class ResultsPage : Container + public class ResultsPage : Container { protected readonly Score Score; protected readonly WorkingBeatmap Beatmap; diff --git a/osu.Game/Screens/Ranking/ResultsPageRanking.cs b/osu.Game/Screens/Ranking/ResultsPageRanking.cs index d316dc7fb4..c9d1061bd1 100644 --- a/osu.Game/Screens/Ranking/ResultsPageRanking.cs +++ b/osu.Game/Screens/Ranking/ResultsPageRanking.cs @@ -12,7 +12,7 @@ using osu.Framework.Graphics.Shapes; namespace osu.Game.Screens.Ranking { - internal class ResultsPageRanking : ResultsPage + public class ResultsPageRanking : ResultsPage { public ResultsPageRanking(Score score, WorkingBeatmap beatmap = null) : base(score, beatmap) { @@ -39,4 +39,4 @@ namespace osu.Game.Screens.Ranking }; } } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Ranking/ResultsPageScore.cs b/osu.Game/Screens/Ranking/ResultsPageScore.cs index bf406ff912..911b688669 100644 --- a/osu.Game/Screens/Ranking/ResultsPageScore.cs +++ b/osu.Game/Screens/Ranking/ResultsPageScore.cs @@ -26,7 +26,7 @@ using osu.Framework.Graphics.Shapes; namespace osu.Game.Screens.Ranking { - internal class ResultsPageScore : ResultsPage + public class ResultsPageScore : ResultsPage { private ScoreCounter scoreCounter; @@ -186,9 +186,9 @@ namespace osu.Game.Screens.Ranking private class DrawableScoreStatistic : Container { - private readonly KeyValuePair statistic; + private readonly KeyValuePair statistic; - public DrawableScoreStatistic(KeyValuePair statistic) + public DrawableScoreStatistic(KeyValuePair statistic) { this.statistic = statistic; @@ -324,7 +324,7 @@ namespace osu.Game.Screens.Ranking title.Colour = artist.Colour = colours.BlueDarker; versionMapper.Colour = colours.Gray8; - versionMapper.Text = $"{beatmap.Version} - mapped by {beatmap.Metadata.Author}"; + versionMapper.Text = $"{beatmap.Version} - mapped by {beatmap.Metadata.Author.Username}"; title.Current = localisation.GetUnicodePreference(beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title); artist.Current = localisation.GetUnicodePreference(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist); } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index abd288baf2..be176c1459 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; using System.Linq; -using osu.Game.Beatmaps.Drawables; using osu.Game.Configuration; using osu.Framework.Input; using OpenTK.Input; @@ -15,469 +14,307 @@ using osu.Framework.MathUtils; using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Caching; using osu.Framework.Threading; using osu.Framework.Configuration; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; +using osu.Game.Screens.Select.Carousel; namespace osu.Game.Screens.Select { - internal class BeatmapCarousel : OsuScrollContainer + public class BeatmapCarousel : OsuScrollContainer { - public BeatmapInfo SelectedBeatmap => selectedPanel?.Beatmap; + /// + /// Triggered when the loaded change and are completely loaded. + /// + public Action BeatmapSetsChanged; + + /// + /// The currently selected beatmap. + /// + public BeatmapInfo SelectedBeatmap => selectedBeatmap?.Beatmap; + + private CarouselBeatmap selectedBeatmap => selectedBeatmapSet?.Beatmaps.FirstOrDefault(s => s.State == CarouselItemState.Selected); + + /// + /// The currently selected beatmap set. + /// + public BeatmapSetInfo SelectedBeatmapSet => selectedBeatmapSet?.BeatmapSet; + + private CarouselBeatmapSet selectedBeatmapSet; + + /// + /// Raised when the is changed. + /// + public Action SelectionChanged; public override bool HandleInput => AllowSelection; - public Action BeatmapsChanged; + private IEnumerable beatmapSets => root.Children.OfType(); - public IEnumerable Beatmaps + public IEnumerable BeatmapSets { - get - { - return groups.Select(g => g.BeatmapSet); - } - + get { return beatmapSets.Select(g => g.BeatmapSet); } set { - scrollableContent.Clear(false); - panels.Clear(); - groups.Clear(); - - List newGroups = null; + CarouselGroup newRoot = new CarouselGroupEagerSelect(); Task.Run(() => { - newGroups = value.Select(createGroup).ToList(); - criteria.Filter(newGroups); - }).ContinueWith(t => - { - Schedule(() => - { - foreach (var g in newGroups) - addGroup(g); + value.Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild); + newRoot.Filter(activeCriteria); - computeYPositions(); - BeatmapsChanged?.Invoke(); - }); - }); + // preload drawables as the ctor overhead is quite high currently. + var _ = newRoot.Drawables; + }).ContinueWith(_ => Schedule(() => + { + root = newRoot; + scrollableContent.Clear(false); + itemsCache.Invalidate(); + scrollPositionCache.Invalidate(); + BeatmapSetsChanged?.Invoke(); + })); } } private readonly List yPositions = new List(); + private Cached itemsCache = new Cached(); + private Cached scrollPositionCache = new Cached(); - /// - /// Required for now unfortunately. - /// - private BeatmapManager manager; + private readonly Container scrollableContent; - private readonly Container scrollableContent; + public Bindable RandomAlgorithm = new Bindable(); + private readonly List previouslyVisitedRandomSets = new List(); + private readonly Stack randomSelectedBeatmaps = new Stack(); - private readonly List groups = new List(); - - private Bindable randomType; - private readonly List seenGroups = new List(); - - private readonly List panels = new List(); - - private readonly Stack> randomSelectedBeatmaps = new Stack>(); - - private BeatmapGroup selectedGroup; - private BeatmapPanel selectedPanel; + protected List Items = new List(); + private CarouselGroup root = new CarouselGroupEagerSelect(); public BeatmapCarousel() { - Add(new OsuContextMenuContainer + Child = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = scrollableContent = new Container + Child = scrollableContent = new Container { RelativeSizeAxes = Axes.X, } + }; + } + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); + } + + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) + { + Schedule(() => + { + var existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); + + if (existingSet == null) + return; + + root.RemoveChild(existingSet); + itemsCache.Invalidate(); }); } - public void AddBeatmap(BeatmapSetInfo beatmapSet) + public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) { - var group = createGroup(beatmapSet); - - //for the time being, let's completely load the difficulty panels in the background. - //this likely won't scale so well, but allows us to completely async the loading flow. - Schedule(delegate + Schedule(() => { - addGroup(group); - computeYPositions(); - if (selectedGroup == null) - selectGroup(group); - }); - } + CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID); - public void RemoveBeatmap(BeatmapSetInfo beatmapSet) => removeGroup(groups.Find(b => b.BeatmapSet.ID == beatmapSet.ID)); + bool hadSelection = existingSet?.State?.Value == CarouselItemState.Selected; - internal void UpdateBeatmap(BeatmapInfo beatmap) - { - // todo: this method should not run more than once for the same BeatmapSetInfo. - var set = manager.Refresh(beatmap.BeatmapSet); + var newSet = createCarouselSet(beatmapSet); - // todo: this method should be smarter as to not recreate panels that haven't changed, etc. - var group = groups.Find(b => b.BeatmapSet.ID == set.ID); + if (existingSet != null) + root.RemoveChild(existingSet); - if (group == null) - return; - - var newGroup = createGroup(set); - - int i = groups.IndexOf(group); - groups.RemoveAt(i); - groups.Insert(i, newGroup); - - if (selectedGroup == group && newGroup.BeatmapPanels.Count == 0) - selectedGroup = null; - - Filter(null, false); - - //check if we can/need to maintain our current selection. - if (selectedGroup == group && newGroup.BeatmapPanels.Count > 0) - { - var newSelection = - newGroup.BeatmapPanels.Find(p => p.Beatmap.ID == selectedPanel?.Beatmap.ID) ?? - newGroup.BeatmapPanels[Math.Min(newGroup.BeatmapPanels.Count - 1, group.BeatmapPanels.IndexOf(selectedPanel))]; - - selectGroup(newGroup, newSelection); - } - } - - public void SelectBeatmap(BeatmapInfo beatmap, bool animated = true) - { - if (beatmap == null || beatmap.Hidden) - { - SelectNext(); - return; - } - - if (beatmap == SelectedBeatmap) return; - - foreach (BeatmapGroup group in groups) - { - var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); - if (panel != null) + if (newSet == null) { - selectGroup(group, panel, animated); + itemsCache.Invalidate(); + SelectNext(); + return; + } + + root.AddChild(newSet); + + applyActiveCriteria(false, false); + + //check if we can/need to maintain our current selection. + if (hadSelection) + select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == selectedBeatmap?.Beatmap.ID) ?? newSet); + + itemsCache.Invalidate(); + }); + } + + public void SelectBeatmap(BeatmapInfo beatmap) + { + if (beatmap?.Hidden != false) + return; + + foreach (CarouselBeatmapSet group in beatmapSets) + { + var item = group.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + if (item != null) + { + select(item); return; } } } - public Action SelectionChanged; - - public Action StartRequested; - - public Action DeleteRequested; - - public Action RestoreRequested; - - public Action HideDifficultyRequested; - + /// + /// Increment selection in the carousel in a chosen direction. + /// + /// The direction to increment. Negative is backwards. + /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (groups.All(g => g.State == BeatmapGroupState.Hidden)) - { - selectedGroup = null; - selectedPanel = null; - SelectionChanged?.Invoke(null); + if (!Items.Any()) return; - } - if (!skipDifficulties && selectedGroup != null) + int originalIndex = Items.IndexOf(selectedBeatmap?.Drawables.First()); + int currentIndex = originalIndex; + + // local function to increment the index in the required direction, wrapping over extremities. + int incrementIndex() => currentIndex = (currentIndex + direction + Items.Count) % Items.Count; + + while (incrementIndex() != originalIndex) { - int i = selectedGroup.BeatmapPanels.IndexOf(selectedPanel) + direction; + var item = Items[currentIndex].Item; - if (i >= 0 && i < selectedGroup.BeatmapPanels.Count) + if (item.Filtered || item.State == CarouselItemState.Selected) continue; + + switch (item) { - //changing difficulty panel, not set. - selectGroup(selectedGroup, selectedGroup.BeatmapPanels[i]); - return; + case CarouselBeatmap beatmap: + if (skipDifficulties) continue; + select(beatmap); + return; + case CarouselBeatmapSet set: + if (skipDifficulties) + select(set); + else + select(direction > 0 ? set.Beatmaps.First(b => !b.Filtered) : set.Beatmaps.Last(b => !b.Filtered)); + return; } } - - int startIndex = Math.Max(0, groups.IndexOf(selectedGroup)); - int index = startIndex; - - do - { - index = (index + direction + groups.Count) % groups.Count; - if (groups[index].State != BeatmapGroupState.Hidden) - { - if (skipDifficulties) - SelectBeatmap(groups[index].SelectedPanel != null ? groups[index].SelectedPanel.Beatmap : groups[index].BeatmapPanels.First().Beatmap); - else - SelectBeatmap(direction == 1 ? groups[index].BeatmapPanels.First().Beatmap : groups[index].BeatmapPanels.Last().Beatmap); - - return; - } - } while (index != startIndex); } - private IEnumerable getVisibleGroups() => groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden); - public void SelectNextRandom() { - if (groups.Count == 0) + var visible = beatmapSets.Where(s => !s.Filtered).ToList(); + if (!visible.Any()) return; - var visibleGroups = getVisibleGroups(); - if (!visibleGroups.Any()) - return; - - if (selectedGroup != null) - randomSelectedBeatmaps.Push(new KeyValuePair(selectedGroup, selectedGroup.SelectedPanel)); - - BeatmapGroup group; - - if (randomType == SelectionRandomType.RandomPermutation) + if (selectedBeatmap != null) { - var notSeenGroups = visibleGroups.Except(seenGroups); - if (!notSeenGroups.Any()) + randomSelectedBeatmaps.Push(selectedBeatmap); + + // when performing a random, we want to add the current set to the previously visited list + // else the user may be "randomised" to the existing selection. + if (previouslyVisitedRandomSets.LastOrDefault() != selectedBeatmapSet) + previouslyVisitedRandomSets.Add(selectedBeatmapSet); + } + + CarouselBeatmapSet set; + + if (RandomAlgorithm == RandomSelectAlgorithm.RandomPermutation) + { + var notYetVisitedSets = visible.Except(previouslyVisitedRandomSets).ToList(); + if (!notYetVisitedSets.Any()) { - seenGroups.Clear(); - notSeenGroups = visibleGroups; + previouslyVisitedRandomSets.Clear(); + notYetVisitedSets = visible; } - group = notSeenGroups.ElementAt(RNG.Next(notSeenGroups.Count())); - seenGroups.Add(group); + set = notYetVisitedSets.ElementAt(RNG.Next(notYetVisitedSets.Count)); + previouslyVisitedRandomSets.Add(set); } else - group = visibleGroups.ElementAt(RNG.Next(visibleGroups.Count())); + set = visible.ElementAt(RNG.Next(visible.Count)); - BeatmapPanel panel = group.BeatmapPanels[RNG.Next(group.BeatmapPanels.Count)]; - - selectGroup(group, panel); + select(set.Beatmaps.Skip(RNG.Next(set.Beatmaps.Count())).FirstOrDefault()); } public void SelectPreviousRandom() { - if (!randomSelectedBeatmaps.Any()) - return; - - var visibleGroups = getVisibleGroups(); - if (!visibleGroups.Any()) - return; - while (randomSelectedBeatmaps.Any()) { - var beatmapCoordinates = randomSelectedBeatmaps.Pop(); - var group = beatmapCoordinates.Key; - if (visibleGroups.Contains(group)) + var beatmap = randomSelectedBeatmaps.Pop(); + + if (!beatmap.Filtered) { - selectGroup(group, beatmapCoordinates.Value); + if (RandomAlgorithm == RandomSelectAlgorithm.RandomPermutation) + previouslyVisitedRandomSets.Remove(selectedBeatmapSet); + select(beatmap); break; } } } - private FilterCriteria criteria = new FilterCriteria(); + private void select(CarouselItem item) + { + if (item == null) return; + item.State.Value = CarouselItemState.Selected; + } - private ScheduledDelegate filterTask; + private FilterCriteria activeCriteria = new FilterCriteria(); + + protected ScheduledDelegate FilterTask; public bool AllowSelection = true; - public void FlushPendingFilters() + public void FlushPendingFilterOperations() { - if (filterTask?.Completed == false) - Filter(null, false); + if (FilterTask?.Completed == false) + applyActiveCriteria(false, false); } - public void Filter(FilterCriteria newCriteria = null, bool debounce = true) + public void Filter(FilterCriteria newCriteria, bool debounce = true) { if (newCriteria != null) - criteria = newCriteria; + activeCriteria = newCriteria; - if (!IsLoaded) return; + applyActiveCriteria(debounce, true); + } - Action perform = delegate + private void applyActiveCriteria(bool debounce, bool scroll) + { + if (root.Children.Any() != true) return; + + void perform() { - filterTask = null; + FilterTask = null; - criteria.Filter(groups); + root.Filter(activeCriteria); + itemsCache.Invalidate(); + if (scroll) scrollPositionCache.Invalidate(); + } - var filtered = new List(groups); - - scrollableContent.Clear(false); - panels.Clear(); - groups.Clear(); - - foreach (var g in filtered) - addGroup(g); - - computeYPositions(); - - if (selectedGroup == null || selectedGroup.State == BeatmapGroupState.Hidden) - SelectNext(); - else - selectGroup(selectedGroup, selectedPanel); - }; - - filterTask?.Cancel(); - filterTask = null; + FilterTask?.Cancel(); + FilterTask = null; if (debounce) - filterTask = Scheduler.AddDelayed(perform, 250); + FilterTask = Scheduler.AddDelayed(perform, 250); else perform(); } - public void ScrollToSelected(bool animated = true) - { - float selectedY = computeYPositions(animated); - ScrollTo(selectedY, animated); - } + private float? scrollTarget; - private BeatmapGroup createGroup(BeatmapSetInfo beatmapSet) - { - foreach (var b in beatmapSet.Beatmaps) - { - if (b.Metadata == null) - b.Metadata = beatmapSet.Metadata; - } - - return new BeatmapGroup(beatmapSet, manager) - { - SelectionChanged = (g, p) => selectGroup(g, p), - StartRequested = b => StartRequested?.Invoke(), - DeleteRequested = b => DeleteRequested?.Invoke(b), - RestoreHiddenRequested = s => RestoreRequested?.Invoke(s), - HideDifficultyRequested = b => HideDifficultyRequested?.Invoke(b), - State = BeatmapGroupState.Collapsed - }; - } - - [BackgroundDependencyLoader(permitNulls: true)] - private void load(BeatmapManager manager, OsuConfigManager config) - { - this.manager = manager; - - randomType = config.GetBindable(OsuSetting.SelectionRandomType); - } - - private void addGroup(BeatmapGroup group) - { - groups.Add(group); - panels.Add(group.Header); - panels.AddRange(group.BeatmapPanels); - } - - private void removeGroup(BeatmapGroup group) - { - if (group == null) - return; - - groups.Remove(group); - panels.Remove(group.Header); - foreach (var p in group.BeatmapPanels) - panels.Remove(p); - - scrollableContent.Remove(group.Header); - scrollableContent.RemoveRange(group.BeatmapPanels); - - if (selectedGroup == group) - SelectNext(); - - computeYPositions(); - } - - /// - /// Computes the target Y positions for every panel in the carousel. - /// - /// The Y position of the currently selected panel. - private float computeYPositions(bool animated = true) - { - yPositions.Clear(); - - float currentY = DrawHeight / 2; - float selectedY = currentY; - - foreach (BeatmapGroup group in groups) - { - movePanel(group.Header, group.State != BeatmapGroupState.Hidden, animated, ref currentY); - - if (group.State == BeatmapGroupState.Expanded) - { - group.Header.MoveToX(-100, 500, Easing.OutExpo); - var headerY = group.Header.Position.Y; - - foreach (BeatmapPanel panel in group.BeatmapPanels) - { - if (panel == selectedPanel) - selectedY = currentY + panel.DrawHeight / 2 - DrawHeight / 2; - - panel.MoveToX(-50, 500, Easing.OutExpo); - - //on first display we want to begin hidden under our group's header. - if (panel.Alpha == 0) - panel.MoveToY(headerY); - - movePanel(panel, true, animated, ref currentY); - } - } - else - { - group.Header.MoveToX(0, 500, Easing.OutExpo); - - foreach (BeatmapPanel panel in group.BeatmapPanels) - { - panel.MoveToX(0, 500, Easing.OutExpo); - movePanel(panel, false, animated, ref currentY); - } - } - } - - currentY += DrawHeight / 2; - scrollableContent.Height = currentY; - - return selectedY; - } - - private void movePanel(Panel panel, bool advance, bool animated, ref float currentY) - { - yPositions.Add(currentY); - panel.MoveToY(currentY, animated ? 750 : 0, Easing.OutExpo); - - if (advance) - currentY += panel.DrawHeight + 5; - } - - private void selectGroup(BeatmapGroup group, BeatmapPanel panel = null, bool animated = true) - { - try - { - if (panel == null) - panel = group.BeatmapPanels.First(); - - if (selectedPanel == panel) return; - - Trace.Assert(group.BeatmapPanels.Contains(panel), @"Selected panel must be in provided group"); - - if (selectedGroup != null && selectedGroup != group && selectedGroup.State != BeatmapGroupState.Hidden) - selectedGroup.State = BeatmapGroupState.Collapsed; - - group.State = BeatmapGroupState.Expanded; - group.SelectedPanel = panel; - - panel.State = PanelSelectedState.Selected; - - if (selectedPanel == panel) return; - - selectedPanel = panel; - selectedGroup = group; - - SelectionChanged?.Invoke(panel.Beatmap); - } - finally - { - ScrollToSelected(animated); - } - } + public void ScrollToSelected() => scrollPositionCache.Invalidate(); protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { @@ -513,57 +350,185 @@ namespace osu.Game.Screens.Select { base.Update(); + if (!itemsCache.IsValid) + updateItems(); + + if (!scrollPositionCache.IsValid) + updateScrollPosition(); + float drawHeight = DrawHeight; - // Remove all panels that should no longer be on-screen - scrollableContent.RemoveAll(delegate (Panel p) - { - float panelPosY = p.Position.Y; - bool remove = panelPosY < Current - p.DrawHeight || panelPosY > Current + drawHeight || !p.IsPresent; - return remove; - }); + // Remove all items that should no longer be on-screen + scrollableContent.RemoveAll(p => p.Y < Current - p.DrawHeight || p.Y > Current + drawHeight || !p.IsPresent); - // Find index range of all panels that should be on-screen - Trace.Assert(panels.Count == yPositions.Count); + // Find index range of all items that should be on-screen + Trace.Assert(Items.Count == yPositions.Count); - int firstIndex = yPositions.BinarySearch(Current - Panel.MAX_HEIGHT); + int firstIndex = yPositions.BinarySearch(Current - DrawableCarouselItem.MAX_HEIGHT); if (firstIndex < 0) firstIndex = ~firstIndex; int lastIndex = yPositions.BinarySearch(Current + drawHeight); - if (lastIndex < 0) - { - lastIndex = ~lastIndex; + if (lastIndex < 0) lastIndex = ~lastIndex; - // Add the first panel of the last visible beatmap group to preload its data. - if (lastIndex != 0 && panels[lastIndex - 1] is BeatmapSetHeader) - lastIndex++; - } + int notVisibleCount = 0; - // Add those panels within the previously found index range that should be displayed. + // Add those items within the previously found index range that should be displayed. for (int i = firstIndex; i < lastIndex; ++i) { - Panel panel = panels[i]; - if (panel.State == PanelSelectedState.Hidden) + DrawableCarouselItem item = Items[i]; + + if (!item.Item.Visible) + { + if (!item.IsPresent) + notVisibleCount++; continue; + } // Only add if we're not already part of the content. - if (!scrollableContent.Contains(panel)) + if (!scrollableContent.Contains(item)) { - // Makes sure headers are always _below_ panels, + // Makes sure headers are always _below_ items, // and depth flows downward. - panel.Depth = i + (panel is BeatmapSetHeader ? panels.Count : 0); - scrollableContent.Add(panel); + item.Depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0); + + switch (item.LoadState) + { + case LoadState.NotLoaded: + LoadComponentAsync(item); + break; + case LoadState.Loading: + break; + default: + scrollableContent.Add(item); + break; + } } } - // Update externally controlled state of currently visible panels + // this is not actually useful right now, but once we have groups may well be. + if (notVisibleCount > 50) + itemsCache.Invalidate(); + + // Update externally controlled state of currently visible items // (e.g. x-offset and opacity). float halfHeight = drawHeight / 2; - foreach (Panel p in scrollableContent.Children) - updatePanel(p, halfHeight); + foreach (DrawableCarouselItem p in scrollableContent.Children) + updateItem(p, halfHeight); + } + + private CarouselBeatmapSet createCarouselSet(BeatmapSetInfo beatmapSet) + { + if (beatmapSet.Beatmaps.All(b => b.Hidden)) + return null; + + // todo: remove the need for this. + foreach (var b in beatmapSet.Beatmaps) + { + if (b.Metadata == null) + b.Metadata = beatmapSet.Metadata; + } + + var set = new CarouselBeatmapSet(beatmapSet); + + foreach (var c in set.Beatmaps) + { + c.State.ValueChanged += v => + { + if (v == CarouselItemState.Selected) + { + selectedBeatmapSet = set; + SelectionChanged?.Invoke(c.Beatmap); + + itemsCache.Invalidate(); + scrollPositionCache.Invalidate(); + } + }; + } + + return set; } /// - /// Computes the x-offset of currently visible panels. Makes the carousel appear round. + /// Computes the target Y positions for every item in the carousel. + /// + /// The Y position of the currently selected item. + private void updateItems() + { + Items = root.Drawables.ToList(); + + yPositions.Clear(); + + float currentY = DrawHeight / 2; + DrawableCarouselBeatmapSet lastSet = null; + + scrollTarget = null; + + foreach (DrawableCarouselItem d in Items) + { + if (d.IsPresent) + { + switch (d) + { + case DrawableCarouselBeatmapSet set: + lastSet = set; + + set.MoveToX(set.Item.State == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); + set.MoveToY(currentY, 750, Easing.OutExpo); + break; + case DrawableCarouselBeatmap beatmap: + if (beatmap.Item.State.Value == CarouselItemState.Selected) + scrollTarget = currentY + beatmap.DrawHeight / 2 - DrawHeight / 2; + + void performMove(float y, float? startY = null) + { + if (startY != null) beatmap.MoveTo(new Vector2(0, startY.Value)); + beatmap.MoveToX(beatmap.Item.State == CarouselItemState.Selected ? -50 : 0, 500, Easing.OutExpo); + beatmap.MoveToY(y, 750, Easing.OutExpo); + } + + Debug.Assert(lastSet != null); + + float? setY = null; + if (!d.IsLoaded || beatmap.Alpha == 0) // can't use IsPresent due to DrawableCarouselItem override. + setY = lastSet.Y + lastSet.DrawHeight + 5; + + if (d.IsLoaded) + performMove(currentY, setY); + else + { + float y = currentY; + d.OnLoadComplete = _ => performMove(y, setY); + } + + break; + } + } + + yPositions.Add(currentY); + + if (d.Item.Visible) + currentY += d.DrawHeight + 5; + } + + currentY += DrawHeight / 2; + scrollableContent.Height = currentY; + + if (selectedBeatmapSet != null && selectedBeatmapSet.State.Value != CarouselItemState.Selected) + { + selectedBeatmapSet = null; + SelectionChanged?.Invoke(null); + } + + itemsCache.Validate(); + } + + private void updateScrollPosition() + { + if (scrollTarget != null) ScrollTo(scrollTarget.Value); + scrollPositionCache.Validate(); + } + + /// + /// Computes the x-offset of currently visible items. Makes the carousel appear round. /// /// /// Vertical distance from the center of the carousel container @@ -581,20 +546,20 @@ namespace osu.Game.Screens.Select } /// - /// Update a panel's x position and multiplicative alpha based on its y position and + /// Update a item's x position and multiplicative alpha based on its y position and /// the current scroll position. /// - /// The panel to be updated. + /// The item to be updated. /// Half the draw height of the carousel container. - private void updatePanel(Panel p, float halfHeight) + private void updateItem(DrawableCarouselItem p, float halfHeight) { var height = p.IsPresent ? p.DrawHeight : 0; - float panelDrawY = p.Position.Y - Current + height / 2; - float dist = Math.Abs(1f - panelDrawY / halfHeight); + float itemDrawY = p.Position.Y - Current + height / 2; + float dist = Math.Abs(1f - itemDrawY / halfHeight); // Setting the origin position serves as an additive position on top of potential - // local transformation we may want to apply (e.g. when a panel gets selected, we + // local transformation we may want to apply (e.g. when a item gets selected, we // may want to smoothly transform it leftwards.) p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0); diff --git a/osu.Game/Screens/Select/BeatmapDetailArea.cs b/osu.Game/Screens/Select/BeatmapDetailArea.cs index a676516300..4403d412fc 100644 --- a/osu.Game/Screens/Select/BeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/BeatmapDetailArea.cs @@ -52,6 +52,7 @@ namespace osu.Game.Screens.Select default: Details.Hide(); + Leaderboard.Scope = (LeaderboardScope)tab - 1; Leaderboard.Show(); break; } diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index a98362e89c..a9a778fe17 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -261,10 +261,11 @@ namespace osu.Game.Screens.Select description.Text = null; source.Text = null; tags.Text = null; + advanced.Beatmap = new BeatmapInfo { StarDifficulty = 0, - Difficulty = new BeatmapDifficulty + BaseDifficulty = new BeatmapDifficulty { CircleSize = 0, DrainRate = 0, @@ -306,36 +307,16 @@ namespace osu.Game.Screens.Select private class MetadataSection : Container { - private readonly TextFlowContainer textFlow; - - public string Text - { - set - { - if (string.IsNullOrEmpty(value)) - { - this.FadeOut(transition_duration); - return; - } - - this.FadeIn(transition_duration); - textFlow.Clear(); - textFlow.AddText(value, s => s.TextSize = 14); - } - } - - public Color4 TextColour - { - get { return textFlow.Colour; } - set { textFlow.Colour = value; } - } + private readonly FillFlowContainer textContainer; + private TextFlowContainer textFlow; public MetadataSection(string title) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Alpha = 0; - InternalChild = new FillFlowContainer + InternalChild = textContainer = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -361,6 +342,44 @@ namespace osu.Game.Screens.Select }, }; } + + public string Text + { + set + { + if (string.IsNullOrEmpty(value)) + { + this.FadeOut(transition_duration); + return; + } + + setTextAsync(value); + } + } + + private void setTextAsync(string text) + { + LoadComponentAsync(new TextFlowContainer(s => s.TextSize = 14) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Colour = textFlow.Colour, + Text = text + }, loaded => + { + textFlow?.Expire(); + textContainer.Add(textFlow = loaded); + + // fade in if we haven't yet. + this.FadeIn(transition_duration); + }); + } + + public Color4 TextColour + { + get { return textFlow.Colour; } + set { textFlow.Colour = value; } + } } private class DimmedLoadingAnimation : VisibilityContainer diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 76c384b84c..3ef6ceeaeb 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -19,14 +19,15 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Cursor; namespace osu.Game.Screens.Select { - internal class BeatmapInfoWedge : OverlayContainer + public class BeatmapInfoWedge : OverlayContainer { private static readonly Vector2 wedged_container_shear = new Vector2(0.15f, 0); - private Drawable beatmapInfoContainer; + private Drawable info; public BeatmapInfoWedge() { @@ -43,12 +44,6 @@ namespace osu.Game.Screens.Select }; } - protected override void LoadComplete() - { - base.LoadComplete(); - AlwaysPresent = true; - } - protected override bool BlockPassThroughMouse => false; protected override void PopIn() @@ -65,56 +60,62 @@ namespace osu.Game.Screens.Select public void UpdateBeatmap(WorkingBeatmap beatmap) { - var lastContainer = beatmapInfoContainer; - float newDepth = lastContainer?.Depth + 1 ?? 0; - - Add(beatmapInfoContainer = new AsyncLoadWrapper( - new BufferedWedgeInfo(beatmap) - { - Shear = -Shear, - OnLoadComplete = d => - { - this.FadeIn(250); - - lastContainer?.FadeOut(250); - lastContainer?.Expire(); - } - }) + LoadComponentAsync(new BufferedWedgeInfo(beatmap) { - Depth = newDepth, + Shear = -Shear, + Depth = info?.Depth + 1 ?? 0, + }, newInfo => + { + // ensure we ourselves are visible if not already. + if (!IsPresent) + this.FadeIn(250); + + info?.FadeOut(250); + info?.Expire(); + + Add(info = newInfo); }); } public class BufferedWedgeInfo : BufferedContainer { - public BufferedWedgeInfo(WorkingBeatmap beatmap) + private readonly WorkingBeatmap working; + + public BufferedWedgeInfo(WorkingBeatmap working) { - BeatmapInfo beatmapInfo = beatmap.BeatmapInfo; - BeatmapMetadata metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); + this.working = working; + } + + [BackgroundDependencyLoader] + private void load() + { + BeatmapInfo beatmapInfo = working.BeatmapInfo; + BeatmapMetadata metadata = beatmapInfo.Metadata ?? working.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); + Beatmap beatmap = working.Beatmap; List labels = new List(); - if (beatmap.Beatmap != null) + if (beatmap != null) { - HitObject lastObject = beatmap.Beatmap.HitObjects.LastOrDefault(); + HitObject lastObject = beatmap.HitObjects.LastOrDefault(); double endTime = (lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0; labels.Add(new InfoLabel(new BeatmapStatistic { Name = "Length", Icon = FontAwesome.fa_clock_o, - Content = beatmap.Beatmap.HitObjects.Count == 0 ? "-" : TimeSpan.FromMilliseconds(endTime - beatmap.Beatmap.HitObjects.First().StartTime).ToString(@"m\:ss"), + Content = beatmap.HitObjects.Count == 0 ? "-" : TimeSpan.FromMilliseconds(endTime - beatmap.HitObjects.First().StartTime).ToString(@"m\:ss"), })); labels.Add(new InfoLabel(new BeatmapStatistic { Name = "BPM", Icon = FontAwesome.fa_circle, - Content = getBPMRange(beatmap.Beatmap), + Content = getBPMRange(beatmap), })); //get statistics from the current ruleset. - labels.AddRange(beatmapInfo.Ruleset.CreateInstance().GetBeatmapStatistics(beatmap).Select(s => new InfoLabel(s))); + labels.AddRange(beatmapInfo.Ruleset.CreateInstance().GetBeatmapStatistics(working).Select(s => new InfoLabel(s))); } PixelSnapping = true; @@ -140,7 +141,7 @@ namespace osu.Game.Screens.Select Children = new[] { // Zoomed-in and cropped beatmap background - new BeatmapBackgroundSprite(beatmap) + new BeatmapBackgroundSprite(working) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -149,7 +150,7 @@ namespace osu.Game.Screens.Select }, }, }, - new DifficultyColourBar(beatmap.BeatmapInfo) + new DifficultyColourBar(beatmapInfo) { RelativeSizeAxes = Axes.Y, Width = 20, @@ -206,19 +207,19 @@ namespace osu.Game.Screens.Select Font = @"Exo2.0-Medium", Text = "mapped by ", TextSize = 15, - }, + }, new OsuSpriteText { Font = @"Exo2.0-Bold", - Text = metadata.Author, + Text = metadata.Author.Username, TextSize = 15, - }, + }, } }, new FillFlowContainer { - Margin = new MarginPadding { Top = 20, Left = 10 }, - Spacing = new Vector2(40, 0), + Margin = new MarginPadding { Top = 20 }, + Spacing = new Vector2(20, 0), AutoSizeAxes = Axes.Both, Children = labels }, @@ -232,50 +233,67 @@ namespace osu.Game.Screens.Select double bpmMax = beatmap.ControlPointInfo.BPMMaximum; double bpmMin = beatmap.ControlPointInfo.BPMMinimum; - if (Precision.AlmostEquals(bpmMin, bpmMax)) return Math.Round(bpmMin) + "bpm"; + if (Precision.AlmostEquals(bpmMin, bpmMax)) return $"{bpmMin:0}"; - return Math.Round(bpmMin) + "-" + Math.Round(bpmMax) + "bpm (mostly " + Math.Round(beatmap.ControlPointInfo.BPMMode) + "bpm)"; + return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.ControlPointInfo.BPMMode:0})"; } - public class InfoLabel : Container + public class InfoLabel : Container, IHasTooltip { + public string TooltipText { get; private set; } + public InfoLabel(BeatmapStatistic statistic) { + TooltipText = statistic.Name; AutoSizeAxes = Axes.Both; + Children = new Drawable[] { - new SpriteIcon - { - Icon = FontAwesome.fa_square, - Origin = Anchor.Centre, - Colour = new Color4(68, 17, 136, 255), - Rotation = 45, - Size = new Vector2(20), - }, - new SpriteIcon - { - Icon = statistic.Icon, - Origin = Anchor.Centre, - Colour = new Color4(255, 221, 85, 255), - Scale = new Vector2(0.8f), - Size = new Vector2(20), - }, - new OsuSpriteText - { - Margin = new MarginPadding { Left = 13 }, - Font = @"Exo2.0-Bold", - Colour = new Color4(255, 221, 85, 255), - Text = statistic.Content, - TextSize = 17, - Origin = Anchor.CentreLeft - }, + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20), + Children = new[] + { + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"441288"), + Icon = FontAwesome.fa_square, + Rotation = 45, + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.8f), + Colour = OsuColour.FromHex(@"f7dd55"), + Icon = statistic.Icon, + }, + } + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = new Color4(255, 221, 85, 255), + Font = @"Exo2.0-Bold", + Margin = new MarginPadding { Left = 30 }, + Text = statistic.Content, + TextSize = 17, + } }; } } private class DifficultyColourBar : DifficultyColouredContainer { - public DifficultyColourBar(BeatmapInfo beatmap) : base(beatmap) + public DifficultyColourBar(BeatmapInfo beatmap) + : base(beatmap) { } @@ -286,21 +304,21 @@ namespace osu.Game.Screens.Select Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = AccentColour, - Width = full_opacity_ratio, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Colour = AccentColour, - Alpha = 0.5f, - X = full_opacity_ratio, - Width = 1 - full_opacity_ratio, - } + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = AccentColour, + Width = full_opacity_ratio, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Colour = AccentColour, + Alpha = 0.5f, + X = full_opacity_ratio, + Width = 1 - full_opacity_ratio, + } }; } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs new file mode 100644 index 0000000000..d7e7b1e265 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.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 System; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Screens.Select.Carousel +{ + public class CarouselBeatmap : CarouselItem + { + public readonly BeatmapInfo Beatmap; + + public CarouselBeatmap(BeatmapInfo beatmap) + { + Beatmap = beatmap; + State.Value = CarouselItemState.Collapsed; + } + + protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this); + + public override void Filter(FilterCriteria criteria) + { + base.Filter(criteria); + + bool match = criteria.Ruleset == null || Beatmap.RulesetID == criteria.Ruleset.ID || Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps; + + if (!string.IsNullOrEmpty(criteria.SearchText)) + match &= + Beatmap.Metadata.SearchableTerms.Any(term => term.IndexOf(criteria.SearchText, StringComparison.InvariantCultureIgnoreCase) >= 0) || + Beatmap.Version.IndexOf(criteria.SearchText, StringComparison.InvariantCultureIgnoreCase) >= 0; + + Filtered.Value = !match; + } + + public override int CompareTo(FilterCriteria criteria, CarouselItem other) + { + if (!(other is CarouselBeatmap otherBeatmap)) + return base.CompareTo(criteria, other); + + switch (criteria.Sort) + { + default: + case SortMode.Difficulty: + var ruleset = Beatmap.RulesetID.CompareTo(otherBeatmap.Beatmap.RulesetID); + if (ruleset != 0) return ruleset; + + return Beatmap.StarDifficulty.CompareTo(otherBeatmap.Beatmap.StarDifficulty); + } + } + + public override string ToString() => Beatmap.ToString(); + } +} diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs new file mode 100644 index 0000000000..885595fc51 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.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; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Screens.Select.Carousel +{ + public class CarouselBeatmapSet : CarouselGroupEagerSelect + { + public IEnumerable Beatmaps => InternalChildren.OfType(); + + public BeatmapSetInfo BeatmapSet; + + public CarouselBeatmapSet(BeatmapSetInfo beatmapSet) + { + BeatmapSet = beatmapSet ?? throw new ArgumentNullException(nameof(beatmapSet)); + + beatmapSet.Beatmaps + .Where(b => !b.Hidden) + .Select(b => new CarouselBeatmap(b)) + .ForEach(AddChild); + } + + protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this); + + public override int CompareTo(FilterCriteria criteria, CarouselItem other) + { + if (!(other is CarouselBeatmapSet otherSet)) + return base.CompareTo(criteria, other); + + switch (criteria.Sort) + { + default: + case SortMode.Artist: + return string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase); + case SortMode.Title: + return string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase); + case SortMode.Author: + return string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.InvariantCultureIgnoreCase); + case SortMode.Difficulty: + return BeatmapSet.MaxStarDifficulty.CompareTo(otherSet.BeatmapSet.MaxStarDifficulty); + } + } + + public override void Filter(FilterCriteria criteria) + { + base.Filter(criteria); + Filtered.Value = InternalChildren.All(i => i.Filtered); + } + + public override string ToString() => BeatmapSet.ToString(); + } +} diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs new file mode 100644 index 0000000000..a54eeb562e --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -0,0 +1,91 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Game.Screens.Select.Carousel +{ + /// + /// A group which ensures only one child is selected. + /// + public class CarouselGroup : CarouselItem + { + private readonly List items; + + protected override DrawableCarouselItem CreateDrawableRepresentation() => null; + + public IReadOnlyList Children => InternalChildren; + + protected List InternalChildren = new List(); + + public override List Drawables + { + get + { + var drawables = base.Drawables; + foreach (var c in InternalChildren) + drawables.AddRange(c.Drawables); + return drawables; + } + } + + public virtual void RemoveChild(CarouselItem i) + { + InternalChildren.Remove(i); + + // it's important we do the deselection after removing, so any further actions based on + // State.ValueChanged make decisions post-removal. + i.State.Value = CarouselItemState.Collapsed; + } + + public virtual void AddChild(CarouselItem i) + { + i.State.ValueChanged += v => ChildItemStateChanged(i, v); + InternalChildren.Add(i); + } + + public CarouselGroup(List items = null) + { + if (items != null) InternalChildren = items; + + State.ValueChanged += v => + { + switch (v) + { + case CarouselItemState.Collapsed: + case CarouselItemState.NotSelected: + InternalChildren.ForEach(c => c.State.Value = CarouselItemState.Collapsed); + break; + case CarouselItemState.Selected: + InternalChildren.ForEach(c => + { + if (c.State == CarouselItemState.Collapsed) c.State.Value = CarouselItemState.NotSelected; + }); + break; + } + }; + } + + public override void Filter(FilterCriteria criteria) + { + base.Filter(criteria); + InternalChildren.Sort((x, y) => x.CompareTo(criteria, y)); + InternalChildren.ForEach(c => c.Filter(criteria)); + } + + protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value) + { + // ensure we are the only item selected + if (value == CarouselItemState.Selected) + { + foreach (var b in InternalChildren) + { + if (item == b) continue; + b.State.Value = CarouselItemState.NotSelected; + } + + State.Value = CarouselItemState.Selected; + } + } + } +} diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs new file mode 100644 index 0000000000..5701760221 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs @@ -0,0 +1,104 @@ +// 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; + +namespace osu.Game.Screens.Select.Carousel +{ + /// + /// A group which ensures at least one child is selected (if the group itself is selected). + /// + public class CarouselGroupEagerSelect : CarouselGroup + { + public CarouselGroupEagerSelect() + { + State.ValueChanged += v => + { + if (v == CarouselItemState.Selected) + attemptSelection(); + }; + } + + /// + /// We need to keep track of the index for cases where the selection is removed but we want to select a new item based on its old location. + /// + private int lastSelectedIndex; + + private CarouselItem lastSelected; + + /// + /// To avoid overhead during filter operations, we don't attempt any selections until after all + /// children have been filtered. This bool will be true during the base + /// operation. + /// + private bool filteringChildren; + + public override void Filter(FilterCriteria criteria) + { + filteringChildren = true; + base.Filter(criteria); + filteringChildren = false; + + attemptSelection(); + } + + public override void RemoveChild(CarouselItem i) + { + base.RemoveChild(i); + + if (i != lastSelected) + updateSelectedIndex(); + } + + public override void AddChild(CarouselItem i) + { + base.AddChild(i); + attemptSelection(); + } + + protected override void ChildItemStateChanged(CarouselItem item, CarouselItemState value) + { + base.ChildItemStateChanged(item, value); + + switch (value) + { + case CarouselItemState.Selected: + updateSelected(item); + break; + case CarouselItemState.NotSelected: + case CarouselItemState.Collapsed: + attemptSelection(); + break; + } + } + + private void attemptSelection() + { + if (filteringChildren) return; + + // we only perform eager selection if we are a currently selected group. + if (State != CarouselItemState.Selected) return; + + // we only perform eager selection if none of our children are in a selected state already. + if (Children.Any(i => i.State == CarouselItemState.Selected)) return; + + CarouselItem nextToSelect = + Children.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered) ?? + Children.Reverse().Skip(InternalChildren.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered); + + if (nextToSelect != null) + nextToSelect.State.Value = CarouselItemState.Selected; + else + updateSelected(null); + } + + private void updateSelected(CarouselItem newSelection) + { + lastSelected = newSelection; + updateSelectedIndex(); + } + + private void updateSelectedIndex() => lastSelectedIndex = lastSelected == null ? 0 : Math.Max(0, InternalChildren.IndexOf(lastSelected)); + } +} diff --git a/osu.Game/Screens/Select/Carousel/CarouselItem.cs b/osu.Game/Screens/Select/Carousel/CarouselItem.cs new file mode 100644 index 0000000000..7d76aee253 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselItem.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 System.Collections.Generic; +using osu.Framework.Configuration; + +namespace osu.Game.Screens.Select.Carousel +{ + public abstract class CarouselItem + { + public readonly BindableBool Filtered = new BindableBool(); + + public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); + + /// + /// This item is not in a hidden state. + /// + public bool Visible => State.Value != CarouselItemState.Collapsed && !Filtered; + + public virtual List Drawables + { + get + { + var items = new List(); + + var self = drawableRepresentation.Value; + if (self?.IsPresent == true) items.Add(self); + + return items; + } + } + + protected CarouselItem() + { + drawableRepresentation = new Lazy(CreateDrawableRepresentation); + + Filtered.ValueChanged += v => + { + if (v && State == CarouselItemState.Selected) + State.Value = CarouselItemState.NotSelected; + }; + } + + private readonly Lazy drawableRepresentation; + + protected abstract DrawableCarouselItem CreateDrawableRepresentation(); + + public virtual void Filter(FilterCriteria criteria) + { + } + + public virtual int CompareTo(FilterCriteria criteria, CarouselItem other) => GetHashCode().CompareTo(other.GetHashCode()); + } + + public enum CarouselItemState + { + Collapsed, + NotSelected, + Selected, + } +} diff --git a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs similarity index 76% rename from osu.Game/Beatmaps/Drawables/BeatmapPanel.cs rename to osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index e216f1b83e..6c0cc341fd 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -2,79 +2,56 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using OpenTK; using OpenTK.Graphics; -using osu.Framework.Input; -using osu.Game.Graphics.Sprites; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -namespace osu.Game.Beatmaps.Drawables +namespace osu.Game.Screens.Select.Carousel { - public class BeatmapPanel : Panel, IHasContextMenu + public class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu { - public BeatmapInfo Beatmap; - private readonly Sprite background; + private readonly BeatmapInfo beatmap; - public Action GainedSelection; - public Action StartRequested; - public Action EditRequested; - public Action HideRequested; + private Sprite background; - private readonly Triangles triangles; - private readonly StarCounter starCounter; + private Action startRequested; + private Action editRequested; + private Action hideRequested; - protected override void Selected() + private Triangles triangles; + private StarCounter starCounter; + + public DrawableCarouselBeatmap(CarouselBeatmap panel) : base(panel) { - base.Selected(); - - GainedSelection?.Invoke(this); - - background.Colour = ColourInfo.GradientVertical( - new Color4(20, 43, 51, 255), - new Color4(40, 86, 102, 255)); - - triangles.Colour = Color4.White; - } - - protected override void Deselected() - { - base.Deselected(); - - background.Colour = new Color4(20, 43, 51, 255); - triangles.Colour = OsuColour.Gray(0.5f); - } - - protected override bool OnClick(InputState state) - { - if (State == PanelSelectedState.Selected) - StartRequested?.Invoke(this); - - return base.OnClick(state); - } - - protected override void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden) - { - if (!IsLoaded) return; - - base.ApplyState(last); - - if (last == PanelSelectedState.Hidden && State != last) - starCounter.ReplayAnimation(); - } - - public BeatmapPanel(BeatmapInfo beatmap) - { - Beatmap = beatmap; + beatmap = panel.Beatmap; Height *= 0.60f; + } + + [BackgroundDependencyLoader(true)] + private void load(SongSelect songSelect, BeatmapManager manager) + { + if (songSelect != null) + { + startRequested = songSelect.Start; + editRequested = songSelect.Edit; + } + + if (manager != null) + hideRequested = manager.Hide; Children = new Drawable[] { @@ -135,7 +112,7 @@ namespace osu.Game.Beatmaps.Drawables new OsuSpriteText { Font = @"Exo2.0-MediumItalic", - Text = $"{(beatmap.Metadata ?? beatmap.BeatmapSet.Metadata).Author}", + Text = $"{(beatmap.Metadata ?? beatmap.BeatmapSet.Metadata).Author.Username}", TextSize = 16, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft @@ -154,11 +131,46 @@ namespace osu.Game.Beatmaps.Drawables }; } + protected override void Selected() + { + base.Selected(); + + background.Colour = ColourInfo.GradientVertical( + new Color4(20, 43, 51, 255), + new Color4(40, 86, 102, 255)); + + triangles.Colour = Color4.White; + } + + protected override void Deselected() + { + base.Deselected(); + + background.Colour = new Color4(20, 43, 51, 255); + triangles.Colour = OsuColour.Gray(0.5f); + } + + protected override bool OnClick(InputState state) + { + if (Item.State == CarouselItemState.Selected) + startRequested?.Invoke(beatmap); + + return base.OnClick(state); + } + + protected override void ApplyState() + { + if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0) + starCounter.ReplayAnimation(); + + base.ApplyState(); + } + public MenuItem[] ContextMenuItems => new MenuItem[] { - new OsuMenuItem("Play", MenuItemType.Highlighted, () => StartRequested?.Invoke(this)), - new OsuMenuItem("Edit", MenuItemType.Standard, () => EditRequested?.Invoke(this)), - new OsuMenuItem("Hide", MenuItemType.Destructive, () => HideRequested?.Invoke(Beatmap)), + new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested?.Invoke(beatmap)), + new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested?.Invoke(beatmap)), + new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested?.Invoke(beatmap)), }; } } diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs similarity index 58% rename from osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs rename to osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index ee75b77747..4b999f9b87 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -4,52 +4,61 @@ using System; using System.Collections.Generic; using System.Linq; -using OpenTK; -using OpenTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; -using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using OpenTK; +using OpenTK.Graphics; -namespace osu.Game.Beatmaps.Drawables +namespace osu.Game.Screens.Select.Carousel { - public class BeatmapSetHeader : Panel, IHasContextMenu + public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu { - public Action GainedSelection; + private Action deleteRequested; + private Action restoreHiddenRequested; + private Action viewDetails; - public Action DeleteRequested; + private readonly BeatmapSetInfo beatmapSet; - public Action RestoreHiddenRequested; - - private readonly SpriteText title; - private readonly SpriteText artist; - - private readonly WorkingBeatmap beatmap; private readonly FillFlowContainer difficultyIcons; - public BeatmapSetHeader(WorkingBeatmap beatmap) + public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) + : base(set) { - this.beatmap = beatmap; + beatmapSet = set.BeatmapSet; + } + + [BackgroundDependencyLoader(true)] + private void load(LocalisationEngine localisation, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay) + { + if (localisation == null) + throw new ArgumentNullException(nameof(localisation)); + + restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore); + deleteRequested = manager.Delete; + if (beatmapOverlay != null) + viewDetails = beatmapOverlay.ShowBeatmapSet; Children = new Drawable[] { new DelayedLoadWrapper( - new PanelBackground(beatmap) + new PanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, - OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), - } - ) - { - TimeBeforeLoad = 300, - }, + OnLoadComplete = d => d.FadeInFromZero(1000, Easing.OutQuint), + }, 300 + ), new FillFlowContainer { Direction = FillDirection.Vertical, @@ -57,39 +66,50 @@ namespace osu.Game.Beatmaps.Drawables AutoSizeAxes = Axes.Both, Children = new Drawable[] { - title = new OsuSpriteText + new OsuSpriteText { Font = @"Exo2.0-BoldItalic", + Current = localisation.GetUnicodePreference(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title), TextSize = 22, Shadow = true, }, - artist = new OsuSpriteText + new OsuSpriteText { Font = @"Exo2.0-SemiBoldItalic", + Current = localisation.GetUnicodePreference(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist), TextSize = 17, Shadow = true, }, - difficultyIcons = new FillFlowContainer + new FillFlowContainer { Margin = new MarginPadding { Top = 5 }, AutoSizeAxes = Axes.Both, + Children = ((CarouselBeatmapSet)Item).Beatmaps.Select(b => new FilterableDifficultyIcon(b)).ToList() } } } }; } - protected override void Selected() + public MenuItem[] ContextMenuItems { - base.Selected(); - GainedSelection?.Invoke(this); - } + get + { + List items = new List(); - [BackgroundDependencyLoader] - private void load(LocalisationEngine localisation) - { - title.Current = localisation.GetUnicodePreference(beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title); - artist.Current = localisation.GetUnicodePreference(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist); + if (Item.State == CarouselItemState.NotSelected) + items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => Item.State.Value = CarouselItemState.Selected)); + + if (beatmapSet.OnlineBeatmapSetID != null) + items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails?.Invoke(beatmapSet.OnlineBeatmapSetID.Value))); + + if (beatmapSet.Beatmaps.Any(b => b.Hidden)) + items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested?.Invoke(beatmapSet))); + + items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => deleteRequested?.Invoke(beatmapSet))); + + return items.ToArray(); + } } private class PanelBackground : BufferedContainer @@ -128,22 +148,19 @@ namespace osu.Game.Beatmaps.Drawables new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal( - Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), + Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), Width = 0.05f, }, new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal( - new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), + 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, - Colour = ColourInfo.GradientHorizontal( - new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), Width = 0.05f, }, } @@ -152,28 +169,17 @@ namespace osu.Game.Beatmaps.Drawables } } - public void AddDifficultyIcons(IEnumerable panels) + public class FilterableDifficultyIcon : DifficultyIcon { - foreach (var p in panels) - difficultyIcons.Add(new DifficultyIcon(p.Beatmap)); - } + private readonly BindableBool filtered = new BindableBool(); - public MenuItem[] ContextMenuItems - { - get + public FilterableDifficultyIcon(CarouselBeatmap item) + : base(item.Beatmap) { - List items = new List(); - - if (State == PanelSelectedState.NotSelected) - items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => State = PanelSelectedState.Selected)); - - if (beatmap.BeatmapSetInfo.Beatmaps.Any(b => b.Hidden)) - items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => RestoreHiddenRequested?.Invoke(beatmap.BeatmapSetInfo))); - - items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => DeleteRequested?.Invoke(beatmap.BeatmapSetInfo))); - - return items.ToArray(); + filtered.BindTo(item.Filtered); + filtered.ValueChanged += v => Schedule(() => this.FadeTo(v ? 0.1f : 1, 100)); + filtered.TriggerChange(); } } } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs new file mode 100644 index 0000000000..cb354b3602 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -0,0 +1,154 @@ +// 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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Game.Graphics; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Select.Carousel +{ + public abstract class DrawableCarouselItem : Container + { + public const float MAX_HEIGHT = 80; + + public override bool RemoveWhenNotAlive => false; + + public override bool IsPresent => base.IsPresent || Item.Visible; + + public readonly CarouselItem Item; + + private Container nestedContainer; + private Container borderContainer; + + private Box hoverLayer; + + protected override Container Content => nestedContainer; + + protected DrawableCarouselItem(CarouselItem item) + { + Item = item; + + Height = MAX_HEIGHT; + RelativeSizeAxes = Axes.X; + Alpha = 0; + } + + private SampleChannel sampleHover; + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + InternalChild = borderContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + BorderColour = new Color4(221, 255, 255, 255), + Children = new Drawable[] + { + nestedContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + hoverLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Blending = BlendingMode.Additive, + }, + } + }; + + sampleHover = audio.Sample.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}"); + hoverLayer.Colour = colours.Blue.Opacity(0.1f); + } + + protected override bool OnHover(InputState state) + { + sampleHover?.Play(); + + hoverLayer.FadeIn(100, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + hoverLayer.FadeOut(1000, Easing.OutQuint); + base.OnHoverLost(state); + } + + public void SetMultiplicativeAlpha(float alpha) => borderContainer.Alpha = alpha; + + protected override void LoadComplete() + { + base.LoadComplete(); + + ApplyState(); + Item.Filtered.ValueChanged += _ => Schedule(ApplyState); + Item.State.ValueChanged += _ => Schedule(ApplyState); + } + + protected virtual void ApplyState() + { + if (!IsLoaded) return; + + switch (Item.State.Value) + { + case CarouselItemState.NotSelected: + Deselected(); + break; + case CarouselItemState.Selected: + Selected(); + break; + } + + if (!Item.Visible) + this.FadeOut(300, Easing.OutQuint); + else + this.FadeIn(250); + } + + protected virtual void Selected() + { + Item.State.Value = CarouselItemState.Selected; + + borderContainer.BorderThickness = 2.5f; + borderContainer.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = new Color4(130, 204, 255, 150), + Radius = 20, + Roundness = 10, + }; + } + + protected virtual void Deselected() + { + Item.State.Value = CarouselItemState.NotSelected; + + borderContainer.BorderThickness = 0; + borderContainer.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(1), + Radius = 10, + Colour = Color4.Black.Opacity(100), + }; + } + + protected override bool OnClick(InputState state) + { + Item.State.Value = CarouselItemState.Selected; + return true; + } + } +} diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index f1215ab33d..3c9cffadfb 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -32,17 +32,17 @@ namespace osu.Game.Screens.Select.Details if ((Beatmap?.Ruleset?.ID ?? 0) == 3) { firstValue.Title = "Key Amount"; - firstValue.Value = (int)Math.Round(Beatmap?.Difficulty?.CircleSize ?? 0); + firstValue.Value = (int)Math.Round(Beatmap?.BaseDifficulty?.CircleSize ?? 0); } else { firstValue.Title = "Circle Size"; - firstValue.Value = Beatmap?.Difficulty?.CircleSize ?? 0; + firstValue.Value = Beatmap?.BaseDifficulty?.CircleSize ?? 0; } - hpDrain.Value = beatmap.Difficulty?.DrainRate ?? 0; - accuracy.Value = beatmap.Difficulty?.OverallDifficulty ?? 0; - approachRate.Value = beatmap.Difficulty?.ApproachRate ?? 0; + hpDrain.Value = beatmap.BaseDifficulty?.DrainRate ?? 0; + accuracy.Value = beatmap.BaseDifficulty?.OverallDifficulty ?? 0; + approachRate.Value = beatmap.BaseDifficulty?.ApproachRate ?? 0; starDifficulty.Value = (float)beatmap.StarDifficulty; } } diff --git a/osu.Game/Screens/Select/Details/UserRatings.cs b/osu.Game/Screens/Select/Details/UserRatings.cs index db6d932464..19bcad367e 100644 --- a/osu.Game/Screens/Select/Details/UserRatings.cs +++ b/osu.Game/Screens/Select/Details/UserRatings.cs @@ -29,11 +29,17 @@ namespace osu.Game.Screens.Select.Details if (value == metrics) return; metrics = value; - var ratings = Metrics.Ratings.ToList(); - negativeRatings.Text = ratings.GetRange(0, ratings.Count / 2).Sum().ToString(); - positiveRatings.Text = ratings.GetRange(ratings.Count / 2, ratings.Count / 2).Sum().ToString(); - ratingsBar.Length = (float)ratings.GetRange(0, ratings.Count / 2).Sum() / ratings.Sum(); - graph.Values = Metrics.Ratings.Select(r => (float)r); + const int rating_range = 10; + + var ratings = Metrics.Ratings.Skip(1).Take(rating_range); // adjust for API returning weird empty data at 0. + + var negativeCount = ratings.Take(rating_range / 2).Sum(); + var totalCount = ratings.Sum(); + + negativeRatings.Text = negativeCount.ToString(); + positiveRatings.Text = (totalCount - negativeCount).ToString(); + ratingsBar.Length = totalCount == 0 ? 0 : (float)negativeCount / totalCount; + graph.Values = ratings.Take(rating_range).Select(r => (float)r); } } diff --git a/osu.Game/Screens/Select/EditSongSelect.cs b/osu.Game/Screens/Select/EditSongSelect.cs index 907c080729..f02d25501e 100644 --- a/osu.Game/Screens/Select/EditSongSelect.cs +++ b/osu.Game/Screens/Select/EditSongSelect.cs @@ -1,14 +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.Input; - namespace osu.Game.Screens.Select { public class EditSongSelect : SongSelect { protected override bool ShowFooter => false; - protected override void OnSelected(InputState state) => Exit(); + protected override void Start() => Exit(); } } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index e83613125b..1b86cec613 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -15,6 +15,7 @@ using osu.Game.Screens.Select.Filter; using Container = osu.Framework.Graphics.Containers.Container; using osu.Framework.Input; using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; using osu.Game.Rulesets; namespace osu.Game.Screens.Select @@ -60,6 +61,7 @@ namespace osu.Game.Screens.Select Group = group, Sort = sort, SearchText = searchTextBox.Text, + AllowConvertedBeatmaps = showConverted, Ruleset = ruleset }; @@ -163,17 +165,24 @@ namespace osu.Game.Screens.Select private readonly Bindable ruleset = new Bindable(); + private Bindable showConverted; + [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours, OsuGame osu) + private void load(OsuColour colours, OsuGame osu, OsuConfigManager config) { sortTabs.AccentColour = colours.GreenLight; + showConverted = config.GetBindable(OsuSetting.ShowConvertedBeatmaps); + showConverted.ValueChanged += val => updateCriteria(); + if (osu != null) ruleset.BindTo(osu.Ruleset); - ruleset.ValueChanged += val => FilterChanged?.Invoke(CreateCriteria()); + ruleset.ValueChanged += val => updateCriteria(); ruleset.TriggerChange(); } + private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; protected override bool OnMouseMove(InputState state) => true; @@ -182,4 +191,4 @@ namespace osu.Game.Screens.Select protected override bool OnDragStart(InputState state) => true; } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index a1fea4a41d..8e99e29c1f 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -1,10 +1,6 @@ // 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.Game.Beatmaps.Drawables; using osu.Game.Rulesets; using osu.Game.Screens.Select.Filter; @@ -16,47 +12,6 @@ namespace osu.Game.Screens.Select public SortMode Sort; public string SearchText; public RulesetInfo Ruleset; - - public void Filter(List groups) - { - foreach (var g in groups) - { - var set = g.BeatmapSet; - - bool hasCurrentMode = set.Beatmaps.Any(bm => bm.RulesetID == (Ruleset?.ID ?? 0)); - - bool match = hasCurrentMode; - - if (!string.IsNullOrEmpty(SearchText)) - match &= set.Metadata.SearchableTerms.Any(term => term.IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) >= 0); - - switch (g.State) - { - case BeatmapGroupState.Hidden: - if (match) g.State = BeatmapGroupState.Collapsed; - break; - default: - if (!match) g.State = BeatmapGroupState.Hidden; - break; - } - } - - switch (Sort) - { - default: - case SortMode.Artist: - groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Artist, y.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase)); - break; - case SortMode.Title: - groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Title, y.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase)); - break; - case SortMode.Author: - groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Author, y.BeatmapSet.Metadata.Author, StringComparison.InvariantCultureIgnoreCase)); - break; - case SortMode.Difficulty: - groups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty)); - break; - } - } + public bool AllowConvertedBeatmaps; } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 00f311e522..40c3cf0fd4 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Menu; namespace osu.Game.Screens.Select { @@ -31,12 +30,9 @@ namespace osu.Game.Screens.Select private const float padding = 80; public Action OnBack; - public Action OnStart; private readonly FillFlowContainer buttons; - public OsuLogo StartButton; - /// Text on the button. /// Colour of the button. /// Hotkey of the button. @@ -106,13 +102,6 @@ namespace osu.Game.Screens.Select Height = 3, Position = new Vector2(0, -3), }, - StartButton = new OsuLogo - { - Anchor = Anchor.BottomRight, - Scale = new Vector2(0.4f), - Position = new Vector2(-70, -25), - Action = () => OnStart?.Invoke() - }, new BackButton { Anchor = Anchor.BottomLeft, @@ -143,8 +132,6 @@ namespace osu.Game.Screens.Select updateModeLight(); } - public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => base.ReceiveMouseInputAt(screenSpacePos) || StartButton.ReceiveMouseInputAt(screenSpacePos); - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; protected override bool OnClick(InputState state) => true; diff --git a/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs b/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs index 6329729687..5bd78d5971 100644 --- a/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs +++ b/osu.Game/Screens/Select/Leaderboards/DrawableRank.cs @@ -14,15 +14,10 @@ namespace osu.Game.Screens.Select.Leaderboards public class DrawableRank : Container { private readonly Sprite rankSprite; + private TextureStore textures; public ScoreRank Rank { get; private set; } - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - rankSprite.Texture = textures.Get($@"Grades/{Rank.GetDescription()}"); - } - public DrawableRank(ScoreRank rank) { Rank = rank; @@ -38,5 +33,20 @@ namespace osu.Game.Screens.Select.Leaderboards }, }; } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + this.textures = textures; + updateTexture(); + } + + private void updateTexture() => rankSprite.Texture = textures.Get($@"Grades/{Rank.GetDescription()}"); + + public void UpdateRank(ScoreRank newRank) + { + Rank = newRank; + updateTexture(); + } } } diff --git a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs index 7d65b8b648..c15a179e8c 100644 --- a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs @@ -17,57 +17,122 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Scoring; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using System.Linq; +using osu.Framework.Configuration; +using osu.Framework.Logging; +using osu.Game.Rulesets; namespace osu.Game.Screens.Select.Leaderboards { public class Leaderboard : Container { + private const double fade_duration = 300; + private readonly ScrollContainer scrollContainer; - private readonly FillFlowContainer scrollFlow; + private readonly Container placeholderContainer; + + private FillFlowContainer scrollFlow; + + private readonly Bindable ruleset = new Bindable(); public Action ScoreSelected; private readonly LoadingAnimation loading; private IEnumerable scores; + public IEnumerable Scores { get { return scores; } set { scores = value; - getScoresRequest?.Cancel(); - int i = 150; - if (scores == null) - { - foreach (var c in scrollFlow.Children) - c.FadeOut(i += 10); + scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); + scrollFlow = null; - foreach (var c in scrollFlow.Children) - c.LifetimeEnd = Time.Current + i; + loading.Hide(); + if (scores == null || !scores.Any()) return; - } - scrollFlow.Clear(); + // ensure placeholder is hidden when displaying scores + PlaceholderState = PlaceholderState.Successful; - i = 0; - foreach (var s in scores) + // schedule because we may not be loaded yet (LoadComponentAsync complains). + Schedule(() => { - var ls = new LeaderboardScore(s, 1 + i) + LoadComponentAsync(new FillFlowContainer { - AlwaysPresent = true, - Action = () => ScoreSelected?.Invoke(s), - State = Visibility.Hidden, - }; - scrollFlow.Add(ls); + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 5f), + Padding = new MarginPadding { Top = 10, Bottom = 5 }, + ChildrenEnumerable = scores.Select((s, index) => new LeaderboardScore(s, index + 1) { Action = () => ScoreSelected?.Invoke(s) }) + }, f => + { + scrollContainer.Add(scrollFlow = f); - using (BeginDelayedSequence(i++ * 50, true)) - ls.Show(); + int i = 0; + foreach (var s in f.Children) + { + using (s.BeginDelayedSequence(i++ * 50, true)) + s.Show(); + } + + scrollContainer.ScrollTo(0f, false); + }); + }); + } + } + + private LeaderboardScope scope; + + public LeaderboardScope Scope + { + get { return scope; } + set + { + if (value == scope) return; + + scope = value; + updateScores(); + } + } + + private PlaceholderState placeholderState; + + protected PlaceholderState PlaceholderState + { + get { return placeholderState; } + set + { + if (value == placeholderState) return; + + switch (placeholderState = value) + { + case PlaceholderState.NetworkFailure: + replacePlaceholder(new RetrievalFailurePlaceholder + { + OnRetry = updateScores, + }); + break; + case PlaceholderState.Unavailable: + replacePlaceholder(new MessagePlaceholder(@"Leaderboards are not available for this beatmap!")); + break; + case PlaceholderState.NoScores: + replacePlaceholder(new MessagePlaceholder(@"No records yet!")); + break; + case PlaceholderState.NotLoggedIn: + replacePlaceholder(new MessagePlaceholder(@"Please login to view online leaderboards!")); + break; + case PlaceholderState.NotSupporter: + replacePlaceholder(new MessagePlaceholder(@"Please invest in a supporter tag to view this leaderboard!")); + break; + default: + replacePlaceholder(null); + break; } - - scrollContainer.ScrollTo(0f, false); } } @@ -79,24 +144,18 @@ namespace osu.Game.Screens.Select.Leaderboards { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, - Children = new Drawable[] - { - scrollFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 5f), - Padding = new MarginPadding { Top = 10, Bottom = 5 }, - }, - }, }, - loading = new LoadingAnimation() + loading = new LoadingAnimation(), + placeholderContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, }; } private APIAccess api; - private BeatmapInfo beatmap; + private OsuGame osuGame; private ScheduledDelegate pendingBeatmapSwitch; @@ -116,33 +175,114 @@ namespace osu.Game.Screens.Select.Leaderboards } [BackgroundDependencyLoader(permitNulls: true)] - private void load(APIAccess api) + private void load(APIAccess api, OsuGame osuGame) { this.api = api; + this.osuGame = osuGame; + + if (osuGame != null) + ruleset.BindTo(osuGame.Ruleset); + + ruleset.ValueChanged += r => updateScores(); + + if (api != null) + api.OnStateChange += handleApiStateChange; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (api != null) + api.OnStateChange -= handleApiStateChange; } private GetScoresRequest getScoresRequest; + private void handleApiStateChange(APIState oldState, APIState newState) + { + if (Scope == LeaderboardScope.Local) + // No need to respond to API state change while current scope is local + return; + + if (newState == APIState.Online) + updateScores(); + } + private void updateScores() { - if (!IsLoaded) return; - - Scores = null; getScoresRequest?.Cancel(); + getScoresRequest = null; + Scores = null; - if (api == null || Beatmap?.OnlineBeatmapID == null) return; + if (Scope == LeaderboardScope.Local) + { + // TODO: get local scores from wherever here. + PlaceholderState = PlaceholderState.NoScores; + return; + } + if (Beatmap?.OnlineBeatmapID == null) + { + PlaceholderState = PlaceholderState.Unavailable; + return; + } + + if (api?.IsLoggedIn != true) + { + PlaceholderState = PlaceholderState.NotLoggedIn; + return; + } + + PlaceholderState = PlaceholderState.Retrieving; loading.Show(); - getScoresRequest = new GetScoresRequest(Beatmap); + if (Scope != LeaderboardScope.Global && !api.LocalUser.Value.IsSupporter) + { + loading.Hide(); + PlaceholderState = PlaceholderState.NotSupporter; + return; + } + + getScoresRequest = new GetScoresRequest(Beatmap, osuGame?.Ruleset.Value ?? Beatmap.Ruleset, Scope); getScoresRequest.Success += r => { Scores = r.Scores; - loading.Hide(); + PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; }; + getScoresRequest.Failure += onUpdateFailed; + api.Queue(getScoresRequest); } + private void onUpdateFailed(Exception e) + { + if (e is OperationCanceledException) return; + + PlaceholderState = PlaceholderState.NetworkFailure; + Logger.Error(e, @"Couldn't fetch beatmap scores!"); + } + + private void replacePlaceholder(Placeholder placeholder) + { + var existingPlaceholder = placeholderContainer.Children.LastOrDefault() as Placeholder; + + if (placeholder != null && placeholder.Equals(existingPlaceholder)) + return; + + existingPlaceholder?.FadeOut(150, Easing.OutQuint).Expire(); + + if (placeholder == null) + return; + + Scores = null; + + placeholderContainer.Add(placeholder); + + placeholder.ScaleTo(0.8f).Then().ScaleTo(1, fade_duration * 3, Easing.OutQuint); + placeholder.FadeInFromZero(fade_duration, Easing.OutQuint); + } + protected override void Update() { base.Update(); @@ -152,6 +292,8 @@ namespace osu.Game.Screens.Select.Leaderboards if (!scrollContainer.IsScrolledToEnd()) fadeStart -= LeaderboardScore.HEIGHT; + if (scrollFlow == null) return; + foreach (var c in scrollFlow.Children) { var topY = c.ToSpaceOfOtherDrawable(Vector2.Zero, scrollFlow).Y; @@ -170,4 +312,23 @@ namespace osu.Game.Screens.Select.Leaderboards } } } + + public enum LeaderboardScope + { + Local, + Country, + Global, + Friend, + } + + public enum PlaceholderState + { + Successful, + Retrieving, + NetworkFailure, + Unavailable, + NoScores, + NotLoggedIn, + NotSupporter, + } } diff --git a/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs index de381cd2dc..03466439ad 100644 --- a/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs @@ -1,10 +1,10 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; +using System.Linq; using OpenTK; using OpenTK.Graphics; -using osu.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,19 +13,16 @@ using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Users; namespace osu.Game.Screens.Select.Leaderboards { - public class LeaderboardScore : OsuClickableContainer, IStateful + public class LeaderboardScore : OsuClickableContainer { public static readonly float HEIGHT = 60; - public event Action StateChanged; - public readonly int RankPosition; public readonly Score Score; @@ -34,72 +31,16 @@ namespace osu.Game.Screens.Select.Leaderboards private const float background_alpha = 0.25f; private const float rank_width = 30; - private readonly Box background; - private readonly Container content; - private readonly Container avatar; - private readonly DrawableRank scoreRank; - private readonly OsuSpriteText nameLabel; - private readonly GlowingSpriteText scoreLabel; - private readonly ScoreComponentLabel maxCombo; - private readonly ScoreComponentLabel accuracy; - private readonly Container flagBadgeContainer; - private readonly FillFlowContainer modsContainer; - - private Visibility state; - - public Visibility State - { - get { return state; } - set - { - if (state == value) - return; - state = value; - - switch (state) - { - case Visibility.Hidden: - foreach (var d in new Drawable[] { avatar, nameLabel, scoreLabel, scoreRank, flagBadgeContainer, maxCombo, accuracy, modsContainer }) - d.FadeOut(); - - Alpha = 0; - - content.MoveToY(75); - avatar.MoveToX(75); - nameLabel.MoveToX(150); - break; - case Visibility.Visible: - this.FadeIn(200); - content.MoveToY(0, 800, Easing.OutQuint); - - using (BeginDelayedSequence(100, true)) - { - avatar.FadeIn(300, Easing.OutQuint); - nameLabel.FadeIn(350, Easing.OutQuint); - - avatar.MoveToX(0, 300, Easing.OutQuint); - nameLabel.MoveToX(0, 350, Easing.OutQuint); - - using (BeginDelayedSequence(250, true)) - { - scoreLabel.FadeIn(200); - scoreRank.FadeIn(200); - - using (BeginDelayedSequence(50, true)) - { - var drawables = new Drawable[] { flagBadgeContainer, maxCombo, accuracy, modsContainer, }; - for (int i = 0; i < drawables.Length; i++) - drawables[i].FadeIn(100 + i * 50); - } - } - } - - break; - } - - StateChanged?.Invoke(State); - } - } + private Box background; + private Container content; + private Drawable avatar; + private DrawableRank scoreRank; + private OsuSpriteText nameLabel; + private GlowingSpriteText scoreLabel; + private ScoreComponentLabel maxCombo; + private ScoreComponentLabel accuracy; + private Container flagBadgeContainer; + private FillFlowContainer modsContainer; public LeaderboardScore(Score score, int rank) { @@ -108,7 +49,11 @@ namespace osu.Game.Screens.Select.Leaderboards RelativeSizeAxes = Axes.X; Height = HEIGHT; + } + [BackgroundDependencyLoader] + private void load() + { Children = new Drawable[] { new Container @@ -152,7 +97,7 @@ namespace osu.Game.Screens.Select.Leaderboards { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(edge_margin), - Children = new Drawable[] + Children = new[] { avatar = new DelayedLoadWrapper( new Avatar(Score.User) @@ -169,7 +114,6 @@ namespace osu.Game.Screens.Select.Leaderboards }, }) { - TimeBeforeLoad = 500, RelativeSizeAxes = Axes.None, Size = new Vector2(HEIGHT - edge_margin * 2, HEIGHT - edge_margin * 2), }, @@ -255,27 +199,51 @@ namespace osu.Game.Screens.Select.Leaderboards Origin = Anchor.BottomRight, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) }, }, }, }, }, }; - - foreach (Mod mod in Score.Mods) - { - modsContainer.Add(new ModIcon(mod) - { - AutoSizeAxes = Axes.Both, - Scale = new Vector2(0.375f) - }); - } } - public void ToggleVisibility() => State = State == Visibility.Visible ? Visibility.Hidden : Visibility.Visible; + public override void Show() + { + foreach (var d in new[] { avatar, nameLabel, scoreLabel, scoreRank, flagBadgeContainer, maxCombo, accuracy, modsContainer }) + d.FadeOut(); - public override void Hide() => State = Visibility.Hidden; - public override void Show() => State = Visibility.Visible; + Alpha = 0; + + content.MoveToY(75); + avatar.MoveToX(75); + nameLabel.MoveToX(150); + + this.FadeIn(200); + content.MoveToY(0, 800, Easing.OutQuint); + + using (BeginDelayedSequence(100, true)) + { + avatar.FadeIn(300, Easing.OutQuint); + nameLabel.FadeIn(350, Easing.OutQuint); + + avatar.MoveToX(0, 300, Easing.OutQuint); + nameLabel.MoveToX(0, 350, Easing.OutQuint); + + using (BeginDelayedSequence(250, true)) + { + scoreLabel.FadeIn(200); + scoreRank.FadeIn(200); + + using (BeginDelayedSequence(50, true)) + { + var drawables = new Drawable[] { flagBadgeContainer, maxCombo, accuracy, modsContainer, }; + for (int i = 0; i < drawables.Length; i++) + drawables[i].FadeIn(100 + i * 50); + } + } + } + } protected override bool OnHover(InputState state) { diff --git a/osu.Game/Screens/Select/Leaderboards/MessagePlaceholder.cs b/osu.Game/Screens/Select/Leaderboards/MessagePlaceholder.cs new file mode 100644 index 0000000000..4f94087d30 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/MessagePlaceholder.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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using OpenTK; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public class MessagePlaceholder : Placeholder + { + private readonly string message; + + public MessagePlaceholder(string message) + { + Direction = FillDirection.Horizontal; + AutoSizeAxes = Axes.Both; + Children = new Drawable[] + { + new SpriteIcon + { + Icon = FontAwesome.fa_exclamation_circle, + Size = new Vector2(26), + Margin = new MarginPadding { Right = 10 }, + }, + new OsuSpriteText + { + Text = this.message = message, + TextSize = 22, + }, + }; + } + + public override bool Equals(Placeholder other) => (other as MessagePlaceholder)?.message == message; + } +} diff --git a/osu.Game/Screens/Select/Leaderboards/Placeholder.cs b/osu.Game/Screens/Select/Leaderboards/Placeholder.cs new file mode 100644 index 0000000000..8427259106 --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/Placeholder.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; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public abstract class Placeholder : FillFlowContainer, IEquatable + { + protected Placeholder() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + public virtual bool Equals(Placeholder other) => GetType() == other?.GetType(); + } +} diff --git a/osu.Game/Screens/Select/Leaderboards/RetrievalFailurePlaceholder.cs b/osu.Game/Screens/Select/Leaderboards/RetrievalFailurePlaceholder.cs new file mode 100644 index 0000000000..7563c08c8b --- /dev/null +++ b/osu.Game/Screens/Select/Leaderboards/RetrievalFailurePlaceholder.cs @@ -0,0 +1,77 @@ +// 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.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using OpenTK; + +namespace osu.Game.Screens.Select.Leaderboards +{ + public class RetrievalFailurePlaceholder : Placeholder + { + public Action OnRetry; + + public RetrievalFailurePlaceholder() + { + Direction = FillDirection.Horizontal; + AutoSizeAxes = Axes.Both; + Children = new Drawable[] + { + new RetryButton + { + Action = () => OnRetry?.Invoke(), + Margin = new MarginPadding { Right = 10 }, + }, + new OsuSpriteText + { + Anchor = Anchor.TopLeft, + Text = @"Couldn't retrieve scores!", + TextSize = 22, + }, + }; + } + + public class RetryButton : OsuHoverContainer + { + private readonly SpriteIcon icon; + + public Action Action; + + public RetryButton() + { + Height = 26; + Width = 26; + Child = new OsuClickableContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => Action?.Invoke(), + Child = icon = new SpriteIcon + { + Icon = FontAwesome.fa_refresh, + Size = new Vector2(26), + Shadow = true, + }, + }; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + icon.ScaleTo(0.8f, 4000, Easing.OutQuint); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + icon.ScaleTo(1, 1000, Easing.OutElastic); + return base.OnMouseUp(state, args); + } + } + } +} diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 2d3b198478..898c195432 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -1,12 +1,10 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Input; - namespace osu.Game.Screens.Select { public class MatchSongSelect : SongSelect { - protected override void OnSelected(InputState state) => Exit(); + protected override void Start() => Exit(); } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index e0a3693371..4a0ee31fbb 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -4,9 +4,10 @@ using System.Linq; using OpenTK.Input; 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.Input; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -42,9 +43,13 @@ namespace osu.Game.Screens.Select beatmapDetails.Leaderboard.ScoreSelected += s => Push(new Results(s)); } + private SampleChannel sampleConfirm; + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { + sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); + Footer.AddButton(@"mods", colours.Yellow, modSelect, Key.F1, float.MaxValue); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.fa_times_circle_o, colours.Purple, null, Key.Number1); @@ -108,11 +113,12 @@ namespace osu.Game.Screens.Select return false; } - protected override void OnSelected(InputState state) + protected override void Start() { if (player != null) return; - if (state?.Keyboard.ControlPressed == true) + // Ctrl+Enter should start map with autoplay enabled. + if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true) { var auto = Ruleset.Value.CreateInstance().GetAutoplayMod(); var autoType = auto.GetType(); @@ -128,6 +134,8 @@ namespace osu.Game.Screens.Select Beatmap.Value.Track.Looping = false; Beatmap.Disabled = true; + sampleConfirm?.Play(); + LoadComponentAsync(player = new PlayerLoader(new Player()), l => Push(player)); } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 836ed465c3..68ee08e721 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -19,31 +19,19 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Menu; using osu.Game.Screens.Select.Options; namespace osu.Game.Screens.Select { public abstract class SongSelect : OsuScreen { - private BeatmapManager manager; - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(); - - private readonly BeatmapCarousel carousel; - private DialogOverlay dialogOverlay; - private static readonly Vector2 wedged_container_size = new Vector2(0.5f, 245); - + private static readonly Vector2 background_blur = new Vector2(20); private const float left_area_padding = 20; - private readonly BeatmapInfoWedge beatmapInfoWedge; - - protected Container LeftContent; - - private static readonly Vector2 background_blur = new Vector2(20); - private CancellationTokenSource initialAddSetsTask; - - private SampleChannel sampleChangeDifficulty; - private SampleChannel sampleChangeBeatmap; + public readonly FilterControl FilterControl; protected virtual bool ShowFooter => true; @@ -63,76 +51,90 @@ namespace osu.Game.Screens.Select /// protected readonly Container FooterPanels; - public readonly FilterControl FilterControl; + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(); + + protected Container LeftContent; + + private readonly BeatmapCarousel carousel; + private readonly BeatmapInfoWedge beatmapInfoWedge; + private DialogOverlay dialogOverlay; + private BeatmapManager beatmaps; + + private SampleChannel sampleChangeDifficulty; + private SampleChannel sampleChangeBeatmap; + + private CancellationTokenSource initialAddSetsTask; + + private DependencyContainer dependencies; + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent); protected SongSelect() { const float carousel_width = 640; const float filter_height = 100; - Add(new ParallaxContainer + AddRange(new Drawable[] { - Padding = new MarginPadding { Top = filter_height }, - ParallaxAmount = 0.005f, - RelativeSizeAxes = Axes.Both, - Children = new[] + new ParallaxContainer { - new WedgeBackground + Padding = new MarginPadding { Top = filter_height }, + ParallaxAmount = 0.005f, + RelativeSizeAxes = Axes.Both, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = carousel_width * 0.76f }, + new WedgeBackground + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = carousel_width * 0.76f }, + } } - } - }); - Add(LeftContent = new Container - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(wedged_container_size.X, 1), - Padding = new MarginPadding - { - Bottom = 50, - Top = wedged_container_size.Y + left_area_padding, - Left = left_area_padding, - Right = left_area_padding * 2, - } - }); - Add(carousel = new BeatmapCarousel - { - RelativeSizeAxes = Axes.Y, - Size = new Vector2(carousel_width, 1), - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - SelectionChanged = carouselSelectionChanged, - BeatmapsChanged = carouselBeatmapsLoaded, - DeleteRequested = promptDelete, - RestoreRequested = s => { foreach (var b in s.Beatmaps) manager.Restore(b); }, - HideDifficultyRequested = b => manager.Hide(b), - StartRequested = () => carouselRaisedStart(), - }); - Add(FilterControl = new FilterControl - { - RelativeSizeAxes = Axes.X, - Height = filter_height, - FilterChanged = criteria => filterChanged(criteria), - Exit = Exit, - }); - Add(beatmapInfoWedge = new BeatmapInfoWedge - { - Alpha = 0, - Size = wedged_container_size, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding - { - Top = left_area_padding, - Right = left_area_padding, }, - }); - Add(new ResetScrollContainer(() => carousel.ScrollToSelected()) - { - RelativeSizeAxes = Axes.Y, - Width = 250, + LeftContent = new Container + { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(wedged_container_size.X, 1), + Padding = new MarginPadding + { + Bottom = 50, + Top = wedged_container_size.Y + left_area_padding, + Left = left_area_padding, + Right = left_area_padding * 2, + } + }, + carousel = new BeatmapCarousel + { + RelativeSizeAxes = Axes.Y, + Size = new Vector2(carousel_width, 1), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + SelectionChanged = carouselSelectionChanged, + BeatmapSetsChanged = carouselBeatmapsLoaded, + }, + FilterControl = new FilterControl + { + RelativeSizeAxes = Axes.X, + Height = filter_height, + FilterChanged = c => carousel.Filter(c), + Exit = Exit, + }, + beatmapInfoWedge = new BeatmapInfoWedge + { + Alpha = 0, + Size = wedged_container_size, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding + { + Top = left_area_padding, + Right = left_area_padding, + }, + }, + new ResetScrollContainer(() => carousel.ScrollToSelected()) + { + RelativeSizeAxes = Axes.Y, + Width = 250, + } }); if (ShowFooter) @@ -151,7 +153,6 @@ namespace osu.Game.Screens.Select Add(Footer = new Footer { OnBack = Exit, - OnStart = () => carouselRaisedStart(), }); FooterPanels.Add(BeatmapOptions = new BeatmapOptionsOverlay()); @@ -161,24 +162,26 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader(permitNulls: true)] private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours) { + dependencies.Cache(this); + if (Footer != null) { Footer.AddButton(@"random", colours.Green, triggerRandom, Key.F2); Footer.AddButton(@"options", colours.Blue, BeatmapOptions, Key.F3); - BeatmapOptions.AddButton(@"Delete", @"Beatmap", FontAwesome.fa_trash, colours.Pink, () => promptDelete(Beatmap.Value.BeatmapSetInfo), Key.Number4, float.MaxValue); + BeatmapOptions.AddButton(@"Delete", @"Beatmap", FontAwesome.fa_trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo), Key.Number4, float.MaxValue); } - if (manager == null) - manager = beatmaps; + if (this.beatmaps == null) + this.beatmaps = beatmaps; if (osu != null) Ruleset.BindTo(osu.Ruleset); - manager.BeatmapSetAdded += onBeatmapSetAdded; - manager.BeatmapSetRemoved += onBeatmapSetRemoved; - manager.BeatmapHidden += onBeatmapHidden; - manager.BeatmapRestored += onBeatmapRestored; + this.beatmaps.BeatmapSetAdded += onBeatmapSetAdded; + this.beatmaps.BeatmapSetRemoved += onBeatmapSetRemoved; + this.beatmaps.BeatmapHidden += onBeatmapHidden; + this.beatmaps.BeatmapRestored += onBeatmapRestored; dialogOverlay = dialog; @@ -187,30 +190,31 @@ namespace osu.Game.Screens.Select initialAddSetsTask = new CancellationTokenSource(); - carousel.Beatmaps = manager.GetAllUsableBeatmapSets(); - - Beatmap.ValueChanged += beatmap_ValueChanged; + carousel.BeatmapSets = this.beatmaps.GetAllUsableBeatmapSets(); Beatmap.DisabledChanged += disabled => carousel.AllowSelection = !disabled; - carousel.AllowSelection = !Beatmap.Disabled; + Beatmap.TriggerChange(); + + Beatmap.ValueChanged += b => + { + if (IsCurrentScreen) + carousel.SelectBeatmap(b?.BeatmapInfo); + }; } - private void onBeatmapRestored(BeatmapInfo b) => carousel.UpdateBeatmap(b); - private void onBeatmapHidden(BeatmapInfo b) => carousel.UpdateBeatmap(b); - - private void carouselBeatmapsLoaded() + public void Edit(BeatmapInfo beatmap) { - if (Beatmap.Value.BeatmapSetInfo?.DeletePending == false) - carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false); - else - carousel.SelectNext(); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap); + Push(new Editor()); } - private void carouselRaisedStart(InputState state = null) + public void Start(BeatmapInfo beatmap) { // if we have a pending filter operation, we want to run it now. // it could change selection (ie. if the ruleset has been changed). - carousel.FlushPendingFilters(); + carousel.FlushPendingFilterOperations(); + + carousel.SelectBeatmap(beatmap); if (selectionChangedDebounce?.Completed == false) { @@ -219,9 +223,14 @@ namespace osu.Game.Screens.Select selectionChangedDebounce = null; } - OnSelected(state); + Start(); } + /// + /// Called when a selection is made. + /// + protected abstract void Start(); + private ScheduledDelegate selectionChangedDebounce; // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. @@ -238,27 +247,24 @@ namespace osu.Game.Screens.Select // In these cases, the other component has already loaded the beatmap, so we don't need to do so again. if (beatmap?.Equals(Beatmap.Value.BeatmapInfo) != true) { - bool preview = beatmap?.BeatmapSetInfoID != Beatmap.Value.BeatmapInfo.BeatmapSetInfoID; + bool preview = beatmap?.BeatmapSetInfoID != Beatmap.Value?.BeatmapInfo.BeatmapSetInfoID; - Beatmap.Value = manager.GetWorkingBeatmap(beatmap, Beatmap); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap); ensurePlayingSelected(preview); } UpdateBeatmap(Beatmap.Value); }; - selectionChangedDebounce?.Cancel(); - if (beatmap?.Equals(beatmapNoDebounce) == true) return; + selectionChangedDebounce?.Cancel(); + beatmapNoDebounce = beatmap; if (beatmap == null) - { - if (!Beatmap.IsDefault) - performLoad(); - } + performLoad(); else { if (beatmap.BeatmapSetInfoID == beatmapNoDebounce?.BeatmapSetInfoID) @@ -269,7 +275,7 @@ namespace osu.Game.Screens.Select if (beatmap == Beatmap.Value.BeatmapInfo) performLoad(); else - selectionChangedDebounce = Scheduler.AddDelayed(performLoad, 100); + selectionChangedDebounce = Scheduler.AddDelayed(performLoad, 200); } } @@ -281,31 +287,49 @@ namespace osu.Game.Screens.Select carousel.SelectNextRandom(); } - protected abstract void OnSelected(InputState state); - - private void filterChanged(FilterCriteria criteria, bool debounce = true) - { - carousel.Filter(criteria, debounce); - } - - private void onBeatmapSetAdded(BeatmapSetInfo s) => Schedule(() => addBeatmapSet(s)); - - private void onBeatmapSetRemoved(BeatmapSetInfo s) => Schedule(() => removeBeatmapSet(s)); - protected override void OnEntering(Screen last) { base.OnEntering(last); Content.FadeInFromZero(250); - FilterControl.Activate(); } - private void beatmap_ValueChanged(WorkingBeatmap beatmap) - { - if (!IsCurrentScreen) return; + private const double logo_transition = 250; - carousel.SelectBeatmap(beatmap?.BeatmapInfo); + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + + logo.RelativePositionAxes = Axes.Both; + Vector2 position = new Vector2(0.95f, 0.96f); + + if (logo.Alpha > 0.8f) + { + logo.MoveTo(position, 500, Easing.OutQuint); + } + else + { + logo.Hide(); + logo.ScaleTo(0.2f); + logo.MoveTo(position); + } + + logo.FadeIn(logo_transition, Easing.OutQuint); + logo.ScaleTo(0.4f, logo_transition, Easing.OutQuint); + + logo.Action = () => + { + Start(); + return false; + }; + } + + protected override void LogoExiting(OsuLogo logo) + { + base.LogoExiting(logo); + logo.ScaleTo(0.2f, logo_transition / 2, Easing.Out); + logo.FadeOut(logo_transition / 2, Easing.Out); } protected override void OnResuming(Screen last) @@ -342,6 +366,7 @@ namespace osu.Game.Screens.Select Content.FadeOut(100); FilterControl.Deactivate(); + return base.OnExiting(next); } @@ -349,12 +374,12 @@ namespace osu.Game.Screens.Select { base.Dispose(isDisposing); - if (manager != null) + if (beatmaps != null) { - manager.BeatmapSetAdded -= onBeatmapSetAdded; - manager.BeatmapSetRemoved -= onBeatmapSetRemoved; - manager.BeatmapHidden -= onBeatmapHidden; - manager.BeatmapRestored -= onBeatmapRestored; + beatmaps.BeatmapSetAdded -= onBeatmapSetAdded; + beatmaps.BeatmapSetRemoved -= onBeatmapSetRemoved; + beatmaps.BeatmapHidden -= onBeatmapHidden; + beatmaps.BeatmapRestored -= onBeatmapRestored; } initialAddSetsTask?.Cancel(); @@ -367,11 +392,10 @@ namespace osu.Game.Screens.Select /// The working beatmap. protected virtual void UpdateBeatmap(WorkingBeatmap beatmap) { - var backgroundModeBeatmap = Background as BackgroundScreenBeatmap; - if (backgroundModeBeatmap != null) + if (Background is BackgroundScreenBeatmap backgroundModeBeatmap) { backgroundModeBeatmap.Beatmap = beatmap; - backgroundModeBeatmap.BlurTo(background_blur, 1000); + backgroundModeBeatmap.BlurTo(background_blur, 750, Easing.OutQuint); backgroundModeBeatmap.FadeTo(1, 250); } @@ -393,20 +417,22 @@ namespace osu.Game.Screens.Select } } - private void addBeatmapSet(BeatmapSetInfo beatmapSet) => carousel.AddBeatmap(beatmapSet); + private void onBeatmapSetAdded(BeatmapSetInfo s) => carousel.UpdateBeatmapSet(s); + private void onBeatmapSetRemoved(BeatmapSetInfo s) => carousel.RemoveBeatmapSet(s); + private void onBeatmapRestored(BeatmapInfo b) => carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); + private void onBeatmapHidden(BeatmapInfo b) => carousel.UpdateBeatmapSet(beatmaps.QueryBeatmapSet(s => s.ID == b.BeatmapSetInfoID)); - private void removeBeatmapSet(BeatmapSetInfo beatmapSet) + private void carouselBeatmapsLoaded() { - carousel.RemoveBeatmap(beatmapSet); - if (carousel.SelectedBeatmap == null) - Beatmap.SetDefault(); + if (Beatmap.Value.BeatmapSetInfo?.DeletePending == false) + carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo); + else + carousel.SelectNextRandom(); } - private void promptDelete(BeatmapSetInfo beatmap) + private void delete(BeatmapSetInfo beatmap) { - if (beatmap == null) - return; - + if (beatmap == null) return; dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } @@ -418,15 +444,16 @@ namespace osu.Game.Screens.Select { case Key.KeypadEnter: case Key.Enter: - carouselRaisedStart(state); + Start(); return true; case Key.Delete: if (state.Keyboard.ShiftPressed) { if (!Beatmap.IsDefault) - promptDelete(Beatmap.Value.BeatmapSetInfo); + delete(Beatmap.Value.BeatmapSetInfo); return true; } + break; } diff --git a/osu.Game/Screens/Tournament/Components/VisualiserContainer.cs b/osu.Game/Screens/Tournament/Components/VisualiserContainer.cs index 3dd7207607..d2b2feb68d 100644 --- a/osu.Game/Screens/Tournament/Components/VisualiserContainer.cs +++ b/osu.Game/Screens/Tournament/Components/VisualiserContainer.cs @@ -12,7 +12,7 @@ using System.Linq; namespace osu.Game.Screens.Tournament.Components { - internal class VisualiserContainer : Container + public class VisualiserContainer : Container { /// /// Number of lines in the visualiser. diff --git a/osu.Game/Screens/Tournament/Drawings.cs b/osu.Game/Screens/Tournament/Drawings.cs index 3d27552212..3e7ab56c99 100644 --- a/osu.Game/Screens/Tournament/Drawings.cs +++ b/osu.Game/Screens/Tournament/Drawings.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Tournament { private const string results_filename = "drawings_results.txt"; - internal override bool ShowOverlays => false; + public override bool ShowOverlays => false; protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); @@ -193,21 +193,21 @@ namespace osu.Game.Screens.Tournament Children = new Drawable[] { - new OsuButton + new TriangleButton { RelativeSizeAxes = Axes.X, Text = "Begin random", Action = teamsContainer.StartScrolling, }, - new OsuButton + new TriangleButton { RelativeSizeAxes = Axes.X, Text = "Stop random", Action = teamsContainer.StopScrolling, }, - new OsuButton + new TriangleButton { RelativeSizeAxes = Axes.X, @@ -232,7 +232,7 @@ namespace osu.Game.Screens.Tournament Children = new Drawable[] { - new OsuButton + new TriangleButton { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 332a6f79cb..c6d9202121 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -6,6 +6,7 @@ using OpenTK.Graphics; using osu.Framework.Graphics; using System.Collections.Generic; using System.Linq; +using Newtonsoft.Json; namespace osu.Game.Storyboards { @@ -23,6 +24,7 @@ namespace osu.Game.Storyboards public CommandTimeline FlipH = new CommandTimeline(); public CommandTimeline FlipV = new CommandTimeline(); + [JsonIgnore] public IEnumerable Timelines { get @@ -39,14 +41,25 @@ namespace osu.Game.Storyboards } } + [JsonIgnore] public double CommandsStartTime => Timelines.Where(t => t.HasCommands).Min(t => t.StartTime); + + [JsonIgnore] public double CommandsEndTime => Timelines.Where(t => t.HasCommands).Max(t => t.EndTime); + + [JsonIgnore] public double CommandsDuration => CommandsEndTime - CommandsStartTime; + [JsonIgnore] public virtual double StartTime => CommandsStartTime; + + [JsonIgnore] public virtual double EndTime => CommandsEndTime; + + [JsonIgnore] public double Duration => EndTime - StartTime; + [JsonIgnore] public bool HasCommands => Timelines.Any(t => t.HasCommands); public virtual IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 59cbe74650..4eca910c1e 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -5,10 +5,11 @@ using osu.Game.Beatmaps; using osu.Game.Storyboards.Drawables; using System.Collections.Generic; using System.Linq; +using System; namespace osu.Game.Storyboards { - public class Storyboard + public class Storyboard : IDisposable { private readonly Dictionary layers = new Dictionary(); public IEnumerable Layers => layers.Values; @@ -59,5 +60,29 @@ namespace osu.Game.Storyboards } return drawable; } + + #region Disposal + + ~Storyboard() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool isDisposed; + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposed) + return; + isDisposed = true; + } + + #endregion } } diff --git a/osu.Game/Tests/Platform/TestStorage.cs b/osu.Game/Tests/Platform/TestStorage.cs index 9f76df2a58..e583183d46 100644 --- a/osu.Game/Tests/Platform/TestStorage.cs +++ b/osu.Game/Tests/Platform/TestStorage.cs @@ -1,12 +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; using osu.Framework.Platform; -using SQLite.Net; -using SQLite.Net.Interop; -using SQLite.Net.Platform.Generic; -using SQLite.Net.Platform.Win32; namespace osu.Game.Tests.Platform { @@ -16,14 +11,9 @@ namespace osu.Game.Tests.Platform { } - public override SQLiteConnection GetDatabase(string name) + public override string GetDatabaseConnectionString(string name) { - ISQLitePlatform platform; - if (RuntimeInfo.IsWindows) - platform = new SQLitePlatformWin32(); - else - platform = new SQLitePlatformGeneric(); - return new SQLiteConnection(platform, @":memory:"); + return "DataSource=:memory:"; } } -} \ No newline at end of file +} diff --git a/osu.Game/Tests/Visual/OsuTestCase.cs b/osu.Game/Tests/Visual/OsuTestCase.cs index d722f7d711..b2c8be47bd 100644 --- a/osu.Game/Tests/Visual/OsuTestCase.cs +++ b/osu.Game/Tests/Visual/OsuTestCase.cs @@ -11,8 +11,11 @@ namespace osu.Game.Tests.Visual { public override void RunTest() { - using (var host = new HeadlessGameHost(AppDomain.CurrentDomain.FriendlyName.Replace(' ', '-'), realtime: false)) + using (var host = new HeadlessGameHost($"test-{Guid.NewGuid()}", realtime: false)) + { + host.Storage.DeleteDirectory(string.Empty); host.Run(new OsuTestCaseTestRunner(this)); + } } public class OsuTestCaseTestRunner : OsuGameBase diff --git a/osu.Game/Tests/Visual/ScreenTestCase.cs b/osu.Game/Tests/Visual/ScreenTestCase.cs new file mode 100644 index 0000000000..2f0831d84a --- /dev/null +++ b/osu.Game/Tests/Visual/ScreenTestCase.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Screens; +using osu.Game.Screens; + +namespace osu.Game.Tests.Visual +{ + /// + /// A test case which can be used to test a screen (that relies on OnEntering being called to execute startup instructions). + /// + public abstract class ScreenTestCase : OsuTestCase + { + private readonly TestOsuScreen baseScreen; + + protected ScreenTestCase() + { + Add(baseScreen = new TestOsuScreen()); + } + + protected void LoadScreen(OsuScreen screen) => baseScreen.LoadScreen(screen); + + public class TestOsuScreen : OsuScreen + { + private OsuScreen nextScreen; + + public void LoadScreen(OsuScreen screen) => Schedule(() => + { + nextScreen = screen; + + if (IsCurrentScreen) + { + Push(screen); + nextScreen = null; + } + else + MakeCurrent(); + }); + + protected override void OnResuming(Screen last) + { + base.OnResuming(last); + if (nextScreen != null) + LoadScreen(nextScreen); + } + } + } +} diff --git a/osu.Game/Tests/Visual/TestCaseEditorMenuBar.cs b/osu.Game/Tests/Visual/TestCaseEditorMenuBar.cs deleted file mode 100644 index 8b5132fe08..0000000000 --- a/osu.Game/Tests/Visual/TestCaseEditorMenuBar.cs +++ /dev/null @@ -1,84 +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; -using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Edit.Menus; - -namespace osu.Game.Tests.Visual -{ - public class TestCaseEditorMenuBar : OsuTestCase - { - public TestCaseEditorMenuBar() - { - Add(new EditorMenuBar - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Y = 50, - Items = new[] - { - new EditorMenuBarItem("File") - { - Items = new[] - { - new EditorMenuItem("Clear All Notes"), - new EditorMenuItem("Open Difficulty..."), - new EditorMenuItem("Save"), - new EditorMenuItem("Create a new Difficulty..."), - new EditorMenuItemSpacer(), - new EditorMenuItem("Revert to Saved"), - new EditorMenuItem("Revert to Saved (Full)"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Test Beatmap"), - new EditorMenuItem("Open AiMod"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Upload Beatmap..."), - new EditorMenuItem("Export Package"), - new EditorMenuItem("Export Map Package"), - new EditorMenuItem("Import from..."), - new EditorMenuItemSpacer(), - new EditorMenuItem("Open Song Folder"), - new EditorMenuItem("Open .osu in Notepad"), - new EditorMenuItem("Open .osb in Notepad"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Exit"), - } - }, - new EditorMenuBarItem("Timing") - { - Items = new[] - { - new EditorMenuItem("Time Signature"), - new EditorMenuItem("Metronome Clicks"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Add Timing Section"), - new EditorMenuItem("Add Inheriting Section"), - new EditorMenuItem("Reset Current Section"), - new EditorMenuItem("Delete Timing Section"), - new EditorMenuItem("Resnap Current Section"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Timing Setup"), - new EditorMenuItemSpacer(), - new EditorMenuItem("Resnap All Notes", MenuItemType.Destructive), - new EditorMenuItem("Move all notes in time...", MenuItemType.Destructive), - new EditorMenuItem("Recalculate Slider Lengths", MenuItemType.Destructive), - new EditorMenuItem("Delete All Timing Sections", MenuItemType.Destructive), - new EditorMenuItemSpacer(), - new EditorMenuItem("Set Current Position as Preview Point"), - } - }, - new EditorMenuBarItem("Testing") - { - Items = new[] - { - new EditorMenuItem("Item 1"), - new EditorMenuItem("Item 2"), - new EditorMenuItem("Item 3"), - } - }, - } - }); - } - } -} diff --git a/osu.Game/Tests/Visual/TestCaseMenuButtonSystem.cs b/osu.Game/Tests/Visual/TestCaseMenuButtonSystem.cs deleted file mode 100644 index 0b50c9cf47..0000000000 --- a/osu.Game/Tests/Visual/TestCaseMenuButtonSystem.cs +++ /dev/null @@ -1,25 +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.Colour; -using osu.Framework.Graphics.Shapes; -using osu.Game.Screens.Menu; -using OpenTK.Graphics; - -namespace osu.Game.Tests.Visual -{ - internal class TestCaseMenuButtonSystem : OsuTestCase - { - public override string Description => @"Main menu button system"; - - public TestCaseMenuButtonSystem() - { - Add(new Box - { - Colour = ColourInfo.GradientVertical(Color4.Gray, Color4.WhiteSmoke), - RelativeSizeAxes = Framework.Graphics.Axes.Both, - }); - Add(new ButtonSystem()); - } - } -} diff --git a/osu.Game/Tests/Visual/TestCaseMenuOverlays.cs b/osu.Game/Tests/Visual/TestCaseMenuOverlays.cs deleted file mode 100644 index e27de96bee..0000000000 --- a/osu.Game/Tests/Visual/TestCaseMenuOverlays.cs +++ /dev/null @@ -1,57 +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.Logging; -using osu.Game.Screens.Play; - -namespace osu.Game.Tests.Visual -{ - internal class TestCaseMenuOverlays : OsuTestCase - { - public override string Description => @"Tests pause and fail overlays"; - - public TestCaseMenuOverlays() - { - FailOverlay failOverlay; - PauseContainer.PauseOverlay pauseOverlay; - - var retryCount = 0; - - Add(pauseOverlay = new PauseContainer.PauseOverlay - { - OnResume = () => Logger.Log(@"Resume"), - OnRetry = () => Logger.Log(@"Retry"), - OnQuit = () => Logger.Log(@"Quit"), - }); - Add(failOverlay = new FailOverlay - { - OnRetry = () => Logger.Log(@"Retry"), - OnQuit = () => Logger.Log(@"Quit"), - }); - - AddStep(@"Pause", delegate - { - if (failOverlay.State == Visibility.Visible) - { - failOverlay.Hide(); - } - pauseOverlay.Show(); - }); - AddStep("Fail", delegate - { - if (pauseOverlay.State == Visibility.Visible) - { - pauseOverlay.Hide(); - } - failOverlay.Show(); - }); - AddStep("Add Retry", delegate - { - retryCount++; - pauseOverlay.Retries = retryCount; - failOverlay.Retries = retryCount; - }); - } - } -} diff --git a/osu.Game/Tests/Visual/TestCaseMods.cs b/osu.Game/Tests/Visual/TestCaseMods.cs deleted file mode 100644 index ef250edcc3..0000000000 --- a/osu.Game/Tests/Visual/TestCaseMods.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Game.Overlays.Mods; -using osu.Game.Rulesets; -using osu.Game.Screens.Play.HUD; -using OpenTK; - -namespace osu.Game.Tests.Visual -{ - internal class TestCaseMods : OsuTestCase - { - public override string Description => @"Mod select overlay and in-game display"; - - private ModSelectOverlay modSelect; - private ModDisplay modDisplay; - - private RulesetStore rulesets; - - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - this.rulesets = rulesets; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Add(modSelect = new ModSelectOverlay - { - RelativeSizeAxes = Axes.X, - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - }); - - Add(modDisplay = new ModDisplay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Position = new Vector2(0, 25), - }); - - modDisplay.Current.BindTo(modSelect.SelectedMods); - - AddStep("Toggle", modSelect.ToggleVisibility); - - foreach (var ruleset in rulesets.AllRulesets) - AddStep(ruleset.CreateInstance().Description, () => modSelect.Ruleset.Value = ruleset); - } - } -} diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..f71bece279 --- /dev/null +++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs @@ -0,0 +1,395 @@ +// 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 OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Caching; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Tests.Visual +{ + public abstract class TestCasePerformancePoints : OsuTestCase + { + protected TestCasePerformancePoints(Ruleset ruleset) + { + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new BeatmapList(ruleset) + } + } + }, + null, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StarRatingGrid() + } + } + }, + null, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new PerformanceList() + } + } + }, + } + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 20), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 20) + } + }; + } + + private class BeatmapList : CompositeDrawable + { + private readonly Container beatmapDisplays; + private readonly Ruleset ruleset; + + public BeatmapList(Ruleset ruleset) + { + this.ruleset = ruleset; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = beatmapDisplays = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 4) + }; + } + + [BackgroundDependencyLoader] + private void load(BeatmapManager beatmaps) + { + var sets = beatmaps.GetAllUsableBeatmapSets(); + var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID < 0 || b.RulesetID == ruleset.LegacyID); + + allBeatmaps.ForEach(b => beatmapDisplays.Add(new BeatmapDisplay(b))); + } + + private class BeatmapDisplay : CompositeDrawable, IHasTooltip + { + private readonly OsuSpriteText text; + private readonly BeatmapInfo beatmap; + + private BeatmapManager beatmaps; + private OsuGameBase osuGame; + + private bool isSelected; + + public string TooltipText => text.Text; + + public BeatmapDisplay(BeatmapInfo beatmap) + { + this.beatmap = beatmap; + + AutoSizeAxes = Axes.Both; + InternalChild = text = new OsuSpriteText(); + } + + protected override bool OnClick(InputState state) + { + if (osuGame.Beatmap.Value.BeatmapInfo.ID == beatmap.ID) + return false; + + osuGame.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + isSelected = true; + return true; + } + + protected override bool OnHover(InputState state) + { + if (isSelected) + return false; + this.FadeColour(Color4.Yellow, 100); + return true; + } + + protected override void OnHoverLost(InputState state) + { + if (isSelected) + return; + this.FadeColour(Color4.White, 100); + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame, BeatmapManager beatmaps) + { + this.osuGame = osuGame; + this.beatmaps = beatmaps; + + var working = beatmaps.GetWorkingBeatmap(beatmap); + text.Text = $"{working.Metadata.Artist} - {working.Metadata.Title} ({working.Metadata.AuthorString}) [{working.BeatmapInfo.Version}]"; + + osuGame.Beatmap.ValueChanged += beatmapChanged; + } + + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + if (isSelected) + this.FadeColour(Color4.White, 100); + isSelected = false; + } + } + } + + private class PerformanceList : CompositeDrawable + { + private readonly FillFlowContainer scores; + private APIAccess api; + + public PerformanceList() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = scores = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 4) + }; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame, APIAccess api) + { + this.api = api; + + if (!api.IsLoggedIn) + { + InternalChild = new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Please login to see online scores", + }; + } + + osuGame.Beatmap.ValueChanged += beatmapChanged; + } + + private GetScoresRequest lastRequest; + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + lastRequest?.Cancel(); + scores.Clear(); + + if (!api.IsLoggedIn) + return; + + lastRequest = new GetScoresRequest(newBeatmap.BeatmapInfo, newBeatmap.BeatmapInfo.Ruleset); + lastRequest.Success += res => res.Scores.ForEach(s => scores.Add(new PerformanceDisplay(s, newBeatmap.Beatmap))); + api.Queue(lastRequest); + } + + private class PerformanceDisplay : CompositeDrawable + { + private readonly OsuSpriteText text; + + private readonly Score score; + private readonly Beatmap beatmap; + + public PerformanceDisplay(Score score, Beatmap beatmap) + { + this.score = score; + this.beatmap = beatmap; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = text = new OsuSpriteText(); + } + + [BackgroundDependencyLoader] + private void load() + { + var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); + var calculator = ruleset.CreatePerformanceCalculator(beatmap, score); + if (calculator == null) + return; + + var attributes = new Dictionary(); + double performance = calculator.Calculate(attributes); + + text.Text = $"{score.User.Username} -> online: {score.PP:n2}pp | local: {performance:n2}pp"; + } + } + } + + private class StarRatingGrid : CompositeDrawable + { + private readonly FillFlowContainer modFlow; + private readonly OsuSpriteText totalText; + private readonly FillFlowContainer categoryTexts; + + public StarRatingGrid() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + modFlow = new FillFlowContainer + { + Name = "Checkbox flow", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(4, 4) + }, + new FillFlowContainer + { + Name = "Information display", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 4), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + totalText = new OsuSpriteText { TextSize = 24 }, + categoryTexts = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame) + { + osuGame.Beatmap.ValueChanged += beatmapChanged; + } + + private Cached informationCache = new Cached(); + + private Ruleset ruleset; + private WorkingBeatmap beatmap; + + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + beatmap = newBeatmap; + + modFlow.Clear(); + + ruleset = newBeatmap.BeatmapInfo.Ruleset.CreateInstance(); + foreach (var mod in ruleset.GetAllMods()) + { + var checkBox = new OsuCheckbox + { + RelativeSizeAxes = Axes.None, + Width = 50, + LabelText = mod.ShortenedName + }; + + checkBox.Current.ValueChanged += v => informationCache.Invalidate(); + modFlow.Add(checkBox); + } + + informationCache.Invalidate(); + } + + protected override void Update() + { + base.Update(); + + if (ruleset == null) + return; + + if (!informationCache.IsValid) + { + totalText.Text = string.Empty; + categoryTexts.Clear(); + + var allMods = ruleset.GetAllMods().ToList(); + Mod[] activeMods = modFlow.Where(c => c.Current.Value).Select(c => allMods.First(m => m.ShortenedName == c.LabelText)).ToArray(); + + var diffCalc = ruleset.CreateDifficultyCalculator(beatmap.Beatmap, activeMods); + if (diffCalc != null) + { + var categories = new Dictionary(); + double totalSr = diffCalc.Calculate(categories); + + totalText.Text = $"Star rating: {totalSr:n2}"; + foreach (var kvp in categories) + categoryTexts.Add(new OsuSpriteText { Text = $"{kvp.Key}: {kvp.Value:n2}" }); + } + + informationCache.Validate(); + } + } + } + } +} diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs index 4a25a52e36..106f0fa8f3 100644 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ b/osu.Game/Tests/Visual/TestCasePlayer.cs @@ -1,13 +1,13 @@ // 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 System.Text; using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; @@ -16,33 +16,44 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { - public class TestCasePlayer : OsuTestCase + public abstract class TestCasePlayer : ScreenTestCase { - protected Player Player; - private RulesetStore rulesets; + private readonly Type ruleset; - public override string Description => @"Showing everything to play the game."; + protected Player Player; + + /// + /// Create a TestCase which runs through the Player screen. + /// + /// An optional ruleset type which we want to target. If not provided we'll allow all rulesets to be tested. + protected TestCasePlayer(Type ruleset) + { + this.ruleset = ruleset; + } + + protected TestCasePlayer() + { + + } [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - this.rulesets = rulesets; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Add(new Box { RelativeSizeAxes = Framework.Graphics.Axes.Both, Colour = Color4.Black, + Depth = int.MaxValue }); - foreach (var r in rulesets.Query()) - AddStep(r.Name, () => loadPlayerFor(r)); + string instantiation = ruleset?.AssemblyQualifiedName; - loadPlayerFor(rulesets.Query().First()); + foreach (var r in rulesets.AvailableRulesets.Where(rs => instantiation == null || rs.InstantiationInfo == instantiation)) + { + Player p = null; + AddStep(r.Name, () => p = loadPlayerFor(r)); + AddUntilStep(() => p.IsLoaded); + } } protected virtual Beatmap CreateBeatmap() @@ -51,12 +62,12 @@ namespace osu.Game.Tests.Visual using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(test_beatmap_data))) using (var reader = new StreamReader(stream)) - beatmap = BeatmapDecoder.GetDecoder(reader).Decode(reader); + beatmap = Game.Beatmaps.Formats.Decoder.GetDecoder(reader).DecodeBeatmap(reader); return beatmap; } - private void loadPlayerFor(RulesetInfo r) + private Player loadPlayerFor(RulesetInfo r) { var beatmap = CreateBeatmap(); @@ -70,19 +81,21 @@ namespace osu.Game.Tests.Visual if (Player != null) Remove(Player); - Add(Player = CreatePlayer(working, instance)); + var player = CreatePlayer(working, instance); + + LoadComponentAsync(player, LoadScreen); + + return player; } - protected virtual Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) + protected virtual Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) => new Player { - return new Player - { - InitialBeatmap = beatmap - }; - } + InitialBeatmap = beatmap, + AllowPause = false + }; private const string test_beatmap_data = -@"osu file format v14 + @"osu file format v14 [General] AudioLeadIn: 500 diff --git a/osu.Game/Tests/Visual/TestCaseSettings.cs b/osu.Game/Tests/Visual/TestCaseSettings.cs deleted file mode 100644 index dfc0b66e21..0000000000 --- a/osu.Game/Tests/Visual/TestCaseSettings.cs +++ /dev/null @@ -1,25 +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.Overlays; - -namespace osu.Game.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.Game/Tests/Visual/TestCaseTextAwesome.cs b/osu.Game/Tests/Visual/TestCaseTextAwesome.cs deleted file mode 100644 index 32934d3c5e..0000000000 --- a/osu.Game/Tests/Visual/TestCaseTextAwesome.cs +++ /dev/null @@ -1,48 +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.MathUtils; -using osu.Game.Graphics; -using OpenTK; -using OpenTK.Graphics; - -namespace osu.Game.Tests.Visual -{ - internal class TestCaseTextAwesome : OsuTestCase - { - public override string Description => @"Tests display of icons"; - - public TestCaseTextAwesome() - { - FillFlowContainer flow; - - Add(flow = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }); - - int i = 50; - foreach (FontAwesome fa in Enum.GetValues(typeof(FontAwesome))) - { - flow.Add(new SpriteIcon - { - Icon = fa, - Size = new Vector2(60), - Colour = new Color4( - Math.Max(0.5f, RNG.NextSingle()), - Math.Max(0.5f, RNG.NextSingle()), - Math.Max(0.5f, RNG.NextSingle()), - 1) - }); - - if (i-- == 0) break; - } - } - } -} diff --git a/osu.Game/Users/Avatar.cs b/osu.Game/Users/Avatar.cs index 111c901ca0..7ced0305fd 100644 --- a/osu.Game/Users/Avatar.cs +++ b/osu.Game/Users/Avatar.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.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -25,6 +26,9 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load(TextureStore textures) { + if (textures == null) + throw new ArgumentNullException(nameof(textures)); + Texture texture = null; if (user != null && user.Id > 1) texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); if (texture == null) texture = textures.Get(@"Online/avatar-guest"); diff --git a/osu.Game/Users/Country.cs b/osu.Game/Users/Country.cs index bf06d9f8bc..0c0d12c1cc 100644 --- a/osu.Game/Users/Country.cs +++ b/osu.Game/Users/Country.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 Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -45,6 +46,9 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load(TextureStore ts) { + if (ts == null) + throw new ArgumentNullException(nameof(ts)); + textures = ts; sprite.Texture = textures.Get($@"Flags/{flagName}"); } diff --git a/osu.Game/Users/UpdateableAvatar.cs b/osu.Game/Users/UpdateableAvatar.cs index 7d304e3bbc..d55c0caad7 100644 --- a/osu.Game/Users/UpdateableAvatar.cs +++ b/osu.Game/Users/UpdateableAvatar.cs @@ -11,7 +11,7 @@ namespace osu.Game.Users /// public class UpdateableAvatar : Container { - private Container displayedAvatar; + private Drawable displayedAvatar; private User user; @@ -40,11 +40,13 @@ namespace osu.Game.Users { displayedAvatar?.FadeOut(300); displayedAvatar?.Expire(); - Add(displayedAvatar = new AsyncLoadWrapper(new Avatar(user) - { - RelativeSizeAxes = Axes.Both, - OnLoadComplete = d => d.FadeInFromZero(200), - })); + Add(displayedAvatar = new DelayedLoadWrapper( + new Avatar(user) + { + RelativeSizeAxes = Axes.Both, + OnLoadComplete = d => d.FadeInFromZero(200), + }) + ); } } -} \ No newline at end of file +} diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 2adb809334..5c0e5f1f95 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -75,7 +75,7 @@ namespace osu.Game.Users public bool Active; [JsonProperty(@"interests")] - public string Intrerests; + public string Interests; [JsonProperty(@"occupation")] public string Occupation; @@ -136,5 +136,7 @@ namespace osu.Game.Users [JsonProperty(@"rankHistory")] public RankHistoryData RankHistory; + + public override string ToString() => Username; } } diff --git a/osu.Game/Users/UserCoverBackground.cs b/osu.Game/Users/UserCoverBackground.cs index c0f0d09d9d..68c97fc8fd 100644 --- a/osu.Game/Users/UserCoverBackground.cs +++ b/osu.Game/Users/UserCoverBackground.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.Framework.Allocation; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -19,6 +20,9 @@ namespace osu.Game.Users [BackgroundDependencyLoader] private void load(TextureStore textures) { + if (textures == null) + throw new ArgumentNullException(nameof(textures)); + if (!string.IsNullOrEmpty(user.CoverUrl)) Texture = textures.Get(user.CoverUrl); } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index e5518b5845..d056afcf54 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -16,10 +16,12 @@ using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; namespace osu.Game.Users { - public class UserPanel : ClickableContainer, IHasContextMenu + public class UserPanel : OsuClickableContainer, IHasContextMenu { private readonly User user; private const float height = 100; @@ -38,8 +40,13 @@ namespace osu.Game.Users public UserPanel(User user) { + if (user == null) + throw new ArgumentNullException(nameof(user)); + this.user = user; + FillFlowContainer infoContainer; + Height = height - status_height; Masking = true; CornerRadius = 5; @@ -52,14 +59,14 @@ namespace osu.Game.Users Children = new Drawable[] { - new AsyncLoadWrapper(new UserCoverBackground(user) + new DelayedLoadWrapper(new UserCoverBackground(user) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, FillMode = FillMode.Fill, OnLoadComplete = d => d.FadeInFromZero(200), - }) { RelativeSizeAxes = Axes.Both }, + }, 0) { RelativeSizeAxes = Axes.Both }, new Box { RelativeSizeAxes = Axes.Both, @@ -97,7 +104,7 @@ namespace osu.Game.Users TextSize = 18, Font = @"Exo2.0-SemiBoldItalic", }, - new FillFlowContainer + infoContainer = new FillFlowContainer { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -112,16 +119,6 @@ namespace osu.Game.Users Width = 30f, RelativeSizeAxes = Axes.Y, }, - new Container - { - Width = 40f, - RelativeSizeAxes = Axes.Y, - }, - new CircularContainer - { - Width = 20f, - RelativeSizeAxes = Axes.Y, - }, }, }, }, @@ -168,11 +165,21 @@ namespace osu.Game.Users }, }, }; + + if (user.IsSupporter) + infoContainer.Add(new SupporterIcon + { + RelativeSizeAxes = Axes.Y, + Width = 20f, + }); } [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuColour colours, UserProfileOverlay profile) { + if (colours == null) + throw new ArgumentNullException(nameof(colours)); + Status.ValueChanged += displayStatus; Status.ValueChanged += status => statusBg.FadeColour(status?.GetAppropriateColour(colours) ?? colours.Gray5, 500, Easing.OutQuint); @@ -213,5 +220,53 @@ namespace osu.Game.Users { new OsuMenuItem("View Profile", MenuItemType.Highlighted, ViewProfile), }; + + private class SupporterIcon : CircularContainer + { + private readonly Box background; + + public SupporterIcon() + { + Masking = true; + Children = new Drawable[] + { + new Box { RelativeSizeAxes = Axes.Both }, + new CircularContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.8f), + Masking = true, + Children = new Drawable[] + { + background = new Box { RelativeSizeAxes = Axes.Both }, + new Triangles + { + TriangleScale = 0.2f, + ColourLight = OsuColour.FromHex(@"ff7db7"), + ColourDark = OsuColour.FromHex(@"de5b95"), + RelativeSizeAxes = Axes.Both, + Velocity = 0.3f, + }, + } + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Icon = FontAwesome.fa_heart, + Scale = new Vector2(0.45f), + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = colours.Pink; + } + } } } diff --git a/osu.Game/app.config b/osu.Game/app.config index a704cc3750..7f2ad68041 100644 --- a/osu.Game/app.config +++ b/osu.Game/app.config @@ -10,6 +10,10 @@ + + + + \ No newline at end of file diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index a6860e4e8c..94678106bf 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -1,13 +1,14 @@  - + + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D} Debug AnyCPU - WinExe + Library Properties osu.Game - osu! + osu.Game 3CF060CD28877D0E3112948951A64B2A7CEEC909 codesigning.pfx false @@ -18,7 +19,6 @@ 3.5 - osu.Game.Program OnOutputUpdated false LocalIntranet @@ -57,13 +57,11 @@ false AnyCPU true - AllRules.ruleset false false false - 6 none @@ -77,7 +75,6 @@ false AnyCPU true - AllRules.ruleset false false @@ -85,71 +82,63 @@ - - lazer.ico - - - Properties\app.manifest - - - true - bin\Debug\ - TRACE;DEBUG - true - 0 - true - full - AnyCPU - false - 6 - prompt - AllRules.ruleset - --tests - false - + - - $(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.dll - True - - - $(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.MsDelta.dll - True - - - $(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.PatchApi.dll - True - $(SolutionDir)\packages\DotNetZip.1.10.1\lib\net20\DotNetZip.dll True - - - $(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll - True + + $(SolutionDir)\packages\Humanizer.Core.2.2.0\lib\netstandard1.0\Humanizer.dll - - $(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll - True + + $(SolutionDir)\packages\Microsoft.Data.Sqlite.Core.2.0.0\lib\netstandard2.0\Microsoft.Data.Sqlite.dll - - $(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll - True + + $(SolutionDir)\packages\Microsoft.EntityFrameworkCore.2.0.0\lib\netstandard2.0\Microsoft.EntityFrameworkCore.dll - - $(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Rocks.dll - True + + $(SolutionDir)\packages\Microsoft.EntityFrameworkCore.Design.2.0.0\lib\net461\Microsoft.EntityFrameworkCore.Design.dll + + + $(SolutionDir)\packages\Microsoft.EntityFrameworkCore.Relational.2.0.0\lib\netstandard2.0\Microsoft.EntityFrameworkCore.Relational.dll + + + $(SolutionDir)\packages\Microsoft.EntityFrameworkCore.Sqlite.Core.2.0.0\lib\netstandard2.0\Microsoft.EntityFrameworkCore.Sqlite.dll + + + $(SolutionDir)\packages\Microsoft.Extensions.Caching.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Abstractions.dll + + + $(SolutionDir)\packages\Microsoft.Extensions.Caching.Memory.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Caching.Memory.dll + + + $(SolutionDir)\packages\Microsoft.Extensions.Configuration.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll + + + $(SolutionDir)\packages\Microsoft.Extensions.DependencyInjection.2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll + + + $(SolutionDir)\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + $(SolutionDir)\packages\Microsoft.Extensions.Logging.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Logging.dll + + + $(SolutionDir)\packages\Microsoft.Extensions.Logging.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll + + + $(SolutionDir)\packages\Microsoft.Extensions.Options.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Options.dll + + + $(SolutionDir)\packages\Microsoft.Extensions.Primitives.2.0.0\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll + $(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll True - - $(SolutionDir)\packages\squirrel.windows.1.7.8\lib\Net45\NuGet.Squirrel.dll - True - $(SolutionDir)\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll True @@ -158,48 +147,58 @@ $(SolutionDir)\packages\OpenTK.3.0.0-git00009\lib\net20\OpenTK.dll True + + $(SolutionDir)\packages\Remotion.Linq.2.1.2\lib\net45\Remotion.Linq.dll + $(SolutionDir)\packages\SharpCompress.0.18.1\lib\net45\SharpCompress.dll True - - $(SolutionDir)\packages\Splat.2.0.0\lib\Net45\Splat.dll + + $(SolutionDir)\packages\SQLitePCLRaw.bundle_green.1.1.8\lib\net45\SQLitePCLRaw.batteries_green.dll True - - $(SolutionDir)\packages\SQLite.Net.Core-PCL.3.1.1\lib\portable-win8+net45+wp8+wpa81+MonoAndroid1+MonoTouch1\SQLite.Net.dll + + $(SolutionDir)\packages\SQLitePCLRaw.bundle_green.1.1.8\lib\net45\SQLitePCLRaw.batteries_v2.dll True - - $(SolutionDir)\packages\SQLite.Net-PCL.3.1.1\lib\net40\SQLite.Net.Platform.Generic.dll + + $(SolutionDir)\packages\SQLitePCLRaw.core.1.1.8\lib\net45\SQLitePCLRaw.core.dll True - - $(SolutionDir)\packages\SQLite.Net-PCL.3.1.1\lib\net4\SQLite.Net.Platform.Win32.dll - True - - - $(SolutionDir)\packages\SQLiteNetExtensions.1.3.0\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1\SQLiteNetExtensions.dll - True - - - $(SolutionDir)\packages\squirrel.windows.1.7.8\lib\Net45\Squirrel.dll + + $(SolutionDir)\packages\SQLitePCLRaw.provider.e_sqlite3.net45.1.1.8\lib\net45\SQLitePCLRaw.provider.e_sqlite3.dll True - - - + + $(SolutionDir)\packages\System.Collections.Immutable.1.4.0\lib\netstandard2.0\System.Collections.Immutable.dll + + + $(SolutionDir)\packages\System.ComponentModel.Annotations.4.4.0\lib\net461\System.ComponentModel.Annotations.dll + + + + + + $(SolutionDir)\packages\System.Diagnostics.DiagnosticSource.4.4.1\lib\net46\System.Diagnostics.DiagnosticSource.dll + + + $(SolutionDir)\packages\System.Interactive.Async.3.1.1\lib\net46\System.Interactive.Async.dll + + + $(SolutionDir)\packages\System.Runtime.CompilerServices.Unsafe.4.4.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + $(SolutionDir)\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll + True + - - osu.licenseheader - - @@ -259,22 +258,90 @@ - + - - - + + + + + + + + + + + + + 20171019041408_InitialCreate.cs + + + + 20171025071459_AddMissingIndexRules.cs + + + + 20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs + + + + 20171209034410_AddRulesetInfoShortName.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + @@ -286,15 +353,16 @@ - + - + + @@ -305,7 +373,6 @@ - @@ -319,7 +386,7 @@ - + @@ -353,6 +420,8 @@ + + @@ -363,7 +432,7 @@ - + @@ -380,14 +449,13 @@ - - + @@ -432,9 +500,10 @@ + - + @@ -507,15 +576,18 @@ - - + + + + - + + @@ -583,6 +655,7 @@ + @@ -614,16 +687,23 @@ + - + + + + + + + @@ -651,12 +731,12 @@ - + - + @@ -684,6 +764,14 @@ + + + + + + + + @@ -697,6 +785,9 @@ + + + @@ -731,49 +822,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - @@ -796,10 +847,9 @@ - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/osu.sln b/osu.sln index 2b1a0aa0e5..356ec4cc7b 100644 --- a/osu.sln +++ b/osu.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.27004.2006 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game", "osu.Game\osu.Game.csproj", "{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}" EndProject @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Desktop.Deploy", "osu.D EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Tests", "osu.Game.Tests\osu.Game.Tests.csproj", "{54377672-20B1-40AF-8087-5CF73BF3953A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Desktop", "osu.Desktop\osu.Desktop.csproj", "{419659FD-72EA-4678-9EB8-B22A746CED70}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,8 +34,8 @@ Global {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.Release|Any CPU.Build.0 = Release|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.VisualTests|Any CPU.ActiveCfg = VisualTests|Any CPU - {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.VisualTests|Any CPU.Build.0 = VisualTests|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Debug|Any CPU.Build.0 = Debug|Any CPU {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -79,10 +81,19 @@ Global {54377672-20B1-40AF-8087-5CF73BF3953A}.Release|Any CPU.Build.0 = Release|Any CPU {54377672-20B1-40AF-8087-5CF73BF3953A}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU {54377672-20B1-40AF-8087-5CF73BF3953A}.VisualTests|Any CPU.Build.0 = Release|Any CPU + {419659FD-72EA-4678-9EB8-B22A746CED70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {419659FD-72EA-4678-9EB8-B22A746CED70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {419659FD-72EA-4678-9EB8-B22A746CED70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {419659FD-72EA-4678-9EB8-B22A746CED70}.Release|Any CPU.Build.0 = Release|Any CPU + {419659FD-72EA-4678-9EB8-B22A746CED70}.VisualTests|Any CPU.ActiveCfg = VisualTests|Any CPU + {419659FD-72EA-4678-9EB8-B22A746CED70}.VisualTests|Any CPU.Build.0 = VisualTests|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668} + EndGlobalSection GlobalSection(MonoDevelopProperties) = preSolution Policies = $0 $0.TextStylePolicy = $1 diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 4011e3991f..2f52881d6d 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -3,6 +3,7 @@ True True True + ExplicitlyExcluded ExplicitlyExcluded SOLUTION HINT @@ -33,6 +34,7 @@ HINT WARNING WARNING + HINT WARNING WARNING DO_NOT_SHOW @@ -43,13 +45,16 @@ WARNING ERROR HINT + HINT HINT WARNING WARNING + HINT DO_NOT_SHOW HINT HINT HINT + HINT WARNING WARNING WARNING @@ -148,6 +153,7 @@ WARNING WARNING WARNING + HINT WARNING WARNING HINT @@ -193,6 +199,8 @@ IPC LTRB MD5 + NS + OS RGB RNG SHA @@ -594,6 +602,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-frame <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aa_bb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> @@ -645,6 +654,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-frame <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True True True True