diff --git a/.gitmodules b/.gitmodules index ee1cc80880..f1c4f5d172 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "osu-framework"] - path = osu-framework - url = https://github.com/ppy/osu-framework [submodule "osu-resources"] path = osu-resources - url = https://github.com/ppy/osu-resources + url = https://github.com/ppy/osu-resources \ No newline at end of file diff --git a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__catch_.xml b/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__catch_.xml index be69e92748..1c988fe6fd 100644 --- a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__catch_.xml +++ b/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__catch_.xml @@ -1,21 +1,17 @@ - diff --git a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__mania_.xml b/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__mania_.xml index da968b54b7..d7bb0f90f1 100644 --- a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__mania_.xml +++ b/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__mania_.xml @@ -1,21 +1,17 @@ - diff --git a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__osu__.xml b/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__osu__.xml index f2b8155e37..997ac6b078 100644 --- a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__osu__.xml +++ b/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__osu__.xml @@ -1,21 +1,17 @@ - diff --git a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__taiko_.xml b/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__taiko_.xml index 941455f8b4..b7a070174c 100644 --- a/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__taiko_.xml +++ b/.idea/.idea.osu/.idea/runConfigurations/RulesetTests__taiko_.xml @@ -1,21 +1,17 @@ - diff --git a/.idea/.idea.osu/.idea/runConfigurations/VisualTests__netcoreapp2_0_.xml b/.idea/.idea.osu/.idea/runConfigurations/VisualTests.xml similarity index 74% rename from .idea/.idea.osu/.idea/runConfigurations/VisualTests__netcoreapp2_0_.xml rename to .idea/.idea.osu/.idea/runConfigurations/VisualTests.xml index 08b4e38667..bf5a1f64e0 100644 --- a/.idea/.idea.osu/.idea/runConfigurations/VisualTests__netcoreapp2_0_.xml +++ b/.idea/.idea.osu/.idea/runConfigurations/VisualTests.xml @@ -1,18 +1,17 @@ - - \ No newline at end of file diff --git a/.idea/.idea.osu/.idea/runConfigurations/VisualTests__net471_.xml b/.idea/.idea.osu/.idea/runConfigurations/VisualTests__net471_.xml deleted file mode 100644 index 20a15f985f..0000000000 --- a/.idea/.idea.osu/.idea/runConfigurations/VisualTests__net471_.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/.idea.osu/.idea/runConfigurations/osu___netcoreapp2_0_.xml b/.idea/.idea.osu/.idea/runConfigurations/osu_.xml similarity index 76% rename from .idea/.idea.osu/.idea/runConfigurations/osu___netcoreapp2_0_.xml rename to .idea/.idea.osu/.idea/runConfigurations/osu_.xml index 2f5c137631..344301d4a7 100644 --- a/.idea/.idea.osu/.idea/runConfigurations/osu___netcoreapp2_0_.xml +++ b/.idea/.idea.osu/.idea/runConfigurations/osu_.xml @@ -1,18 +1,17 @@ - - \ No newline at end of file diff --git a/.idea/.idea.osu/.idea/runConfigurations/osu___net471_.xml b/.idea/.idea.osu/.idea/runConfigurations/osu___net471_.xml deleted file mode 100644 index 7196e486d2..0000000000 --- a/.idea/.idea.osu/.idea/runConfigurations/osu___net471_.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index df5b11f63a..ed67fa92cc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,110 +2,54 @@ "version": "0.2.0", "configurations": [ { - "name": "VisualTests (Debug, net471)", - "windows": { - "type": "clr" - }, - "type": "mono", - "request": "launch", - "program": "${workspaceRoot}/osu.Game.Tests/bin/Debug/net471/osu.Game.Tests.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Debug, msbuild)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - }, - { - "name": "VisualTests (Release, net471)", - "windows": { - "type": "clr" - }, - "type": "mono", - "request": "launch", - "program": "${workspaceRoot}/osu.Game.Tests/bin/Debug/net471/osu.Game.Tests.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, msbuild)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - }, - { - "name": "osu! (Debug, net471)", - "windows": { - "type": "clr" - }, - "type": "mono", - "request": "launch", - "program": "${workspaceRoot}/osu.Desktop/bin/Debug/net471/osu!.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Debug, msbuild)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - }, - { - "name": "osu! (Release, net471)", - "windows": { - "type": "clr" - }, - "type": "mono", - "request": "launch", - "program": "${workspaceRoot}/osu.Desktop/bin/Release/net471/osu!.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, msbuild)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - }, - { - "name": "VisualTests (Debug, netcoreapp2.0)", + "name": "VisualTests (Debug)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.0/osu.Game.Tests.dll" + "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.1/osu.Game.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Debug, dotnet)", + "preLaunchTask": "Build tests (Debug)", "env": {}, "console": "internalConsole" }, { - "name": "VisualTests (Release, netcoreapp2.0)", + "name": "VisualTests (Release)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.0/osu.Game.Tests.dll" + "${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.1/osu.Game.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, dotnet)", + "preLaunchTask": "Build tests (Release)", "env": {}, "console": "internalConsole" }, { - "name": "osu! (Debug, netcoreapp2.0)", + "name": "osu! (Debug)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.0/osu!.dll", + "${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.1/osu!.dll", ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Debug, dotnet)", + "preLaunchTask": "Build osu! (Debug)", "env": {}, "console": "internalConsole" }, { - "name": "osu! (Release, netcoreapp2.0)", + "name": "osu! (Release)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.0/osu!.dll", + "${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.1/osu!.dll", ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, dotnet)", + "preLaunchTask": "Build osu! (Release)", "env": {}, "console": "internalConsole" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7144a584f3..188f20b69f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,41 +4,14 @@ "version": "2.0.0", "tasks": [ { - "label": "Build (Debug, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Release, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "/p:Configuration=Release", - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Debug, dotnet)", + "label": "Build osu! (Debug)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Desktop", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:GenerateFullPaths=true", "/m", "/verbosity:m" @@ -47,14 +20,14 @@ "problemMatcher": "$msCompile" }, { - "label": "Build (Release, dotnet)", + "label": "Build osu! (Release)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Desktop", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:Configuration=Release", "/p:GenerateFullPaths=true", "/m", @@ -64,16 +37,40 @@ "problemMatcher": "$msCompile" }, { - "label": "Restore (net471)", + "label": "Build tests (Debug)", "type": "shell", - "command": "nuget", + "command": "dotnet", "args": [ - "restore" + "build", + "--no-restore", + "osu.Game.Tests", + "/p:TargetFramework=netcoreapp2.1", + "/p:GenerateFullPaths=true", + "/m", + "/verbosity:m" ], - "problemMatcher": [] + "group": "build", + "problemMatcher": "$msCompile" }, { - "label": "Restore (netcoreapp2.0)", + "label": "Build tests (Release)", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "--no-restore", + "osu.Game.Tests", + "/p:TargetFramework=netcoreapp2.1", + "/p:Configuration=Release", + "/p:GenerateFullPaths=true", + "/m", + "/verbosity:m" + ], + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "Restore (netcoreapp2.1)", "type": "shell", "command": "dotnet", "args": [ diff --git a/COMPILING.md b/COMPILING.md deleted file mode 100644 index bfcbf6bc2c..0000000000 --- a/COMPILING.md +++ /dev/null @@ -1,36 +0,0 @@ -# Linux -### 1. Requirements: -Mono >= 5.4.0 (>= 5.8.0 recommended) -Please check [here](http://www.mono-project.com/download/) for stable or [here](http://www.mono-project.com/download/alpha/) for an alpha release. -NuGet >= 4.4.0 -msbuild -git - -### 2. Cloning project -Clone the entire repository with submodules using -``` -git clone https://github.com/ppy/osu --recursive -``` -Then restore NuGet packages from the repository -``` -nuget restore -``` -### 3. Compiling -Simply run `msbuild` where `osu.sln` is located, this will create all binaries in `osu/osu.Desktop/bin/Debug`. -### 4. Optimizing -If you want additional performance you can change build type to Release with -``` -msbuild -p:Configuration=Release -``` -Additionally, mono provides an AOT utility which attempts to precompile binaries. You can utilize that by running -``` -mono --aot ./osu\!.exe -``` -### 5. Troubleshooting -You may run into trouble with NuGet versioning, as the one in packaging system is almost always out of date. Simply run -``` -nuget -sudo nuget update -self -``` -**Warning** NuGet creates few config files when it's run for the first time. -Do not run NuGet as root on the first run or you might run into very peculiar issues. diff --git a/appveyor.yml b/appveyor.yml index 69bc762f4c..ac6d6ebff8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,11 +2,6 @@ clone_depth: 1 version: '{branch}-{build}' image: Visual Studio 2017 configuration: Debug -cache: - - C:\ProgramData\chocolatey\bin -> appveyor.yml - - C:\ProgramData\chocolatey\lib -> appveyor.yml - - inspectcode -> appveyor.yml - - packages -> **\packages.config install: - cmd: git submodule update --init --recursive --depth=5 - cmd: choco install resharper-clt -y diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml index cadebd9d11..0247714cdf 100644 --- a/appveyor_deploy.yml +++ b/appveyor_deploy.yml @@ -1,29 +1,24 @@ -branches: - only: - - release -skip_tags: true -skip_branch_with_pr: true clone_depth: 1 -version: '{branch}-{build}' +version: '{build}' +skip_non_tags: true image: Visual Studio 2017 -configuration: Debug -cache: - - packages -> **\packages.config install: - - cmd: git submodule update --init --recursive --depth=5 + - git clone https://github.com/ppy/osu-deploy before_build: + - ps: if($env:appveyor_repo_tag -eq 'True') { Update-AppveyorBuild -Version $env:appveyor_repo_tag_name } + - cmd: git submodule update --init --recursive --depth=5 - cmd: nuget restore -verbosity quiet -build: - project: osu.Desktop.Deploy/osu.Desktop.Deploy.csproj - verbosity: minimal -after_build: +build_script: - ps: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/secure-file/master/install.ps1')) - appveyor DownloadFile https://puu.sh/A6g5K/4d08705438.enc # signing certificate - cmd: appveyor-tools\secure-file -decrypt 4d08705438.enc -secret %decode_secret% -out %HOMEPATH%\deanherbert.pfx - appveyor DownloadFile https://puu.sh/A6g75/fdc6f19b04.enc # deploy configuration - - cmd: appveyor-tools\secure-file -decrypt fdc6f19b04.enc -secret %decode_secret% -out osu.Desktop.Deploy\bin\Debug\net471\osu.Desktop.Deploy.exe.config - - cd osu.Desktop.Deploy\bin\Debug\net471\ - - osu.Desktop.Deploy.exe %code_signing_password% + - cd osu-deploy + - nuget restore -verbosity quiet + - msbuild osu.Desktop.Deploy.csproj + - cmd: ..\appveyor-tools\secure-file -decrypt ..\fdc6f19b04.enc -secret %decode_secret% -out bin\Debug\net471\osu.Desktop.Deploy.exe.config + - cd bin\Debug\net471\ + - osu.Desktop.Deploy.exe %code_signing_password% %APPVEYOR_REPO_TAG_NAME% environment: TargetFramework: net471 decode_secret: @@ -31,4 +26,7 @@ environment: code_signing_password: secure: 34tLNqvjmmZEi97MLKfrnQ== artifacts: - - path: 'Releases\*' \ No newline at end of file + - path: 'Releases\*' +deploy: + - provider: Environment + name: github \ No newline at end of file diff --git a/osu-framework b/osu-framework deleted file mode 160000 index fac688633b..0000000000 --- a/osu-framework +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fac688633b8fcf34ae5d0514c26b03e217161eb4 diff --git a/osu.Desktop.Deploy/App.config b/osu.Desktop.Deploy/App.config deleted file mode 100644 index 9ec53d5a31..0000000000 --- a/osu.Desktop.Deploy/App.config +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/osu.Desktop.Deploy/GitHubRelease.cs b/osu.Desktop.Deploy/GitHubRelease.cs deleted file mode 100644 index faf312d4f7..0000000000 --- a/osu.Desktop.Deploy/GitHubRelease.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using Newtonsoft.Json; - -namespace osu.Desktop.Deploy -{ - public class GitHubRelease - { - [JsonProperty(@"id")] - public int Id; - - [JsonProperty(@"tag_name")] - public string TagName => $"v{Name}"; - - [JsonProperty(@"name")] - public string Name; - - [JsonProperty(@"draft")] - public bool Draft; - - [JsonProperty(@"prerelease")] - public bool PreRelease; - - [JsonProperty(@"upload_url")] - public string UploadUrl; - } -} diff --git a/osu.Desktop.Deploy/Program.cs b/osu.Desktop.Deploy/Program.cs deleted file mode 100644 index a1c2a8aef2..0000000000 --- a/osu.Desktop.Deploy/Program.cs +++ /dev/null @@ -1,471 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Management.Automation; -using Newtonsoft.Json; -using osu.Framework.IO.Network; -using FileWebRequest = osu.Framework.IO.Network.FileWebRequest; -using WebRequest = osu.Framework.IO.Network.WebRequest; - -namespace osu.Desktop.Deploy -{ - internal static class Program - { - private static string packages => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); - private static string nugetPath => Path.Combine(packages, @"nuget.commandline\4.5.1\tools\NuGet.exe"); - private static string squirrelPath => Path.Combine(packages, @"squirrel.windows\1.8.0\tools\Squirrel.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"]; - public static string GitHubAccessToken = ConfigurationManager.AppSettings["GitHubAccessToken"]; - public static string GitHubUsername = ConfigurationManager.AppSettings["GitHubUsername"]; - public static string GitHubRepoName = ConfigurationManager.AppSettings["GitHubRepoName"]; - public static string SolutionName = ConfigurationManager.AppSettings["SolutionName"]; - public static string ProjectName = ConfigurationManager.AppSettings["ProjectName"]; - public static string NuSpecName = ConfigurationManager.AppSettings["NuSpecName"]; - public static string TargetNames = ConfigurationManager.AppSettings["TargetName"]; - public static string PackageName = ConfigurationManager.AppSettings["PackageName"]; - public static string IconName = ConfigurationManager.AppSettings["IconName"]; - public static string CodeSigningCertificate = ConfigurationManager.AppSettings["CodeSigningCertificate"]; - - public static string GitHubApiEndpoint => $"https://api.github.com/repos/{GitHubUsername}/{GitHubRepoName}/releases"; - public static string GitHubReleasePage => $"https://github.com/{GitHubUsername}/{GitHubRepoName}/releases"; - - /// - /// How many previous build deltas we want to keep when publishing. - /// - private const int keep_delta_count = 4; - - private static string codeSigningCmd => string.IsNullOrEmpty(codeSigningPassword) ? "" : $"-n \"/a /f {codeSigningCertPath} /p {codeSigningPassword} /t http://timestamp.comodoca.com/authenticode\""; - - private static string homeDir => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - private static string codeSigningCertPath => Path.Combine(homeDir, CodeSigningCertificate); - private static string solutionPath => Environment.CurrentDirectory; - private static string stagingPath => Path.Combine(solutionPath, StagingFolder); - private static string iconPath => Path.Combine(solutionPath, ProjectName, IconName); - - private static string nupkgFilename(string ver) => $"{PackageName}.{ver}.nupkg"; - private static string nupkgDistroFilename(string ver) => $"{PackageName}-{ver}-full.nupkg"; - - private static readonly Stopwatch sw = new Stopwatch(); - - private static string codeSigningPassword; - - private static bool interactive; - - public static void Main(string[] args) - { - interactive = args.Length == 0; - - displayHeader(); - - findSolutionPath(); - - if (!Directory.Exists(ReleasesFolder)) - { - write("WARNING: No release directory found. Make sure you want this!", ConsoleColor.Yellow); - Directory.CreateDirectory(ReleasesFolder); - } - - checkGitHubReleases(); - - refreshDirectory(StagingFolder); - - //increment build number until we have a unique one. - string verBase = DateTime.Now.ToString("yyyy.Mdd."); - int increment = 0; - while (Directory.GetFiles(ReleasesFolder, $"*{verBase}{increment}*").Any()) - increment++; - - string version = $"{verBase}{increment}"; - - Console.ForegroundColor = ConsoleColor.White; - Console.Write($"Ready to deploy {version}!"); - pauseIfInteractive(); - - sw.Start(); - - if (!string.IsNullOrEmpty(CodeSigningCertificate)) - { - Console.Write("Enter code signing password: "); - codeSigningPassword = args.Length > 0 ? args[0] : readLineMasked(); - } - - write("Updating AssemblyInfo..."); - updateCsprojVersion(version); - updateAppveyorVersion(version); - - write("Running build process..."); - foreach (string targetName in TargetNames.Split(',')) - runCommand(msbuild_path, $"/v:quiet /m /t:{targetName.Replace('.', '_')} /p:OutputPath={stagingPath};Targets=\"Clean;Build\";Configuration=Release {SolutionName}.sln"); - - write("Creating NuGet deployment package..."); - runCommand(nugetPath, $"pack {NuSpecName} -Version {version} -Properties Configuration=Deploy -OutputDirectory {stagingPath} -BasePath {stagingPath}"); - - //prune once before checking for files so we can avoid erroring on files which aren't even needed for this build. - pruneReleases(); - - checkReleaseFiles(); - - write("Running squirrel build..."); - runCommand(squirrelPath, $"--releasify {stagingPath}\\{nupkgFilename(version)} --framework-version=net471 --setupIcon {iconPath} --icon {iconPath} {codeSigningCmd} --no-msi"); - - //prune again to clean up before upload. - pruneReleases(); - - //rename setup to install. - File.Copy(Path.Combine(ReleasesFolder, "Setup.exe"), Path.Combine(ReleasesFolder, "install.exe"), true); - File.Delete(Path.Combine(ReleasesFolder, "Setup.exe")); - - uploadBuild(version); - - //reset assemblyinfo. - updateCsprojVersion("0.0.0"); - - write("Done!", ConsoleColor.White); - pauseIfInteractive(); - } - - private static void displayHeader() - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(); - Console.WriteLine(" Please note that OSU! and PPY are registered trademarks and as such covered by trademark law."); - Console.WriteLine(" Do not distribute builds of this project publicly that make use of these."); - Console.ResetColor(); - Console.WriteLine(); - } - - /// - /// Ensure we have all the files in the release directory which are expected to be there. - /// This should have been accounted for in earlier steps, and just serves as a verification step. - /// - private static void checkReleaseFiles() - { - if (!canGitHub) return; - - var releaseLines = getReleaseLines(); - - //ensure we have all files necessary - foreach (var l in releaseLines) - if (!File.Exists(Path.Combine(ReleasesFolder, l.Filename))) - error($"Local file missing {l.Filename}"); - } - - private static IEnumerable getReleaseLines() => File.ReadAllLines(Path.Combine(ReleasesFolder, "RELEASES")).Select(l => new ReleaseLine(l)); - - private static void pruneReleases() - { - if (!canGitHub) return; - - write("Pruning RELEASES..."); - - var releaseLines = getReleaseLines().ToList(); - - var fulls = releaseLines.Where(l => l.Filename.Contains("-full")).Reverse().Skip(1); - - //remove any FULL releases (except most recent) - foreach (var l in fulls) - { - write($"- Removing old release {l.Filename}", ConsoleColor.Yellow); - File.Delete(Path.Combine(ReleasesFolder, l.Filename)); - releaseLines.Remove(l); - } - - //remove excess deltas - var deltas = releaseLines.Where(l => l.Filename.Contains("-delta")).ToArray(); - if (deltas.Length > keep_delta_count) - { - foreach (var l in deltas.Take(deltas.Length - keep_delta_count)) - { - write($"- Removing old delta {l.Filename}", ConsoleColor.Yellow); - File.Delete(Path.Combine(ReleasesFolder, l.Filename)); - releaseLines.Remove(l); - } - } - - var lines = new List(); - releaseLines.ForEach(l => lines.Add(l.ToString())); - File.WriteAllLines(Path.Combine(ReleasesFolder, "RELEASES"), lines); - } - - private static void uploadBuild(string version) - { - if (!canGitHub || string.IsNullOrEmpty(CodeSigningCertificate)) - return; - - write("Publishing to GitHub..."); - - write($"- Creating release {version}...", ConsoleColor.Yellow); - var req = new JsonWebRequest($"{GitHubApiEndpoint}") - { - Method = HttpMethod.POST, - }; - req.AddRaw(JsonConvert.SerializeObject(new GitHubRelease - { - Name = version, - Draft = true, - PreRelease = true - })); - req.AuthenticatedBlockingPerform(); - - var assetUploadUrl = req.ResponseObject.UploadUrl.Replace("{?name,label}", "?name={0}"); - foreach (var a in Directory.GetFiles(ReleasesFolder).Reverse()) //reverse to upload RELEASES first. - { - write($"- Adding asset {a}...", ConsoleColor.Yellow); - var upload = new WebRequest(assetUploadUrl, Path.GetFileName(a)) - { - Method = HttpMethod.POST, - Timeout = 240000, - ContentType = "application/octet-stream", - }; - - upload.AddRaw(File.ReadAllBytes(a)); - upload.AuthenticatedBlockingPerform(); - } - - openGitHubReleasePage(); - } - - 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(); - - var lastRelease = req.ResponseObject.FirstOrDefault(); - - if (lastRelease == null) - return; - - if (lastRelease.Draft) - { - openGitHubReleasePage(); - error("There's a pending draft release! You probably don't want to push a build with this present."); - } - - //there's a previous release for this project. - var assetReq = new JsonWebRequest>($"{GitHubApiEndpoint}/{lastRelease.Id}/assets"); - assetReq.AuthenticatedBlockingPerform(); - var assets = assetReq.ResponseObject; - - //make sure our RELEASES file is the same as the last build on the server. - var releaseAsset = assets.FirstOrDefault(a => a.Name == "RELEASES"); - - //if we don't have a RELEASES asset then the previous release likely wasn't a Squirrel one. - if (releaseAsset == null) return; - - write($"Last GitHub release was {lastRelease.Name}."); - - bool requireDownload = false; - - if (!File.Exists(Path.Combine(ReleasesFolder, nupkgDistroFilename(lastRelease.Name)))) - { - write("Last version's package not found locally.", ConsoleColor.Red); - requireDownload = true; - } - else - { - var lastReleases = new RawFileWebRequest($"{GitHubApiEndpoint}/assets/{releaseAsset.Id}"); - lastReleases.AuthenticatedBlockingPerform(); - if (File.ReadAllText(Path.Combine(ReleasesFolder, "RELEASES")) != lastReleases.ResponseString) - { - write("Server's RELEASES differed from ours.", ConsoleColor.Red); - requireDownload = true; - } - } - - if (!requireDownload) return; - - write("Refreshing local releases directory..."); - refreshDirectory(ReleasesFolder); - - foreach (var a in assets) - { - if (a.Name.EndsWith(".exe")) continue; - - write($"- Downloading {a.Name}...", ConsoleColor.Yellow); - new FileWebRequest(Path.Combine(ReleasesFolder, a.Name), $"{GitHubApiEndpoint}/assets/{a.Id}").AuthenticatedBlockingPerform(); - } - } - - private static void refreshDirectory(string directory) - { - if (Directory.Exists(directory)) - Directory.Delete(directory, true); - Directory.CreateDirectory(directory); - } - - private static void updateCsprojVersion(string version) - { - var toUpdate = new[] { "", "" }; - string file = Path.Combine(ProjectName, $"{ProjectName}.csproj"); - - var l1 = File.ReadAllLines(file); - List l2 = new List(); - foreach (var l in l1) - { - string line = l; - - foreach (var tag in toUpdate) - { - int startIndex = l.IndexOf(tag, StringComparison.InvariantCulture); - if (startIndex == -1) - continue; - startIndex += tag.Length; - - int endIndex = l.IndexOf("<", startIndex, StringComparison.InvariantCulture); - line = $"{l.Substring(0, startIndex)}{version}{l.Substring(endIndex)}"; - } - - l2.Add(line); - } - - File.WriteAllLines(file, l2); - } - - /// - /// Find the base path of the active solution (git checkout location) - /// - private static void findSolutionPath() - { - string path = Path.GetDirectoryName(Environment.CommandLine.Replace("\"", "").Trim()); - - if (string.IsNullOrEmpty(path)) - path = Environment.CurrentDirectory; - - while (!File.Exists(Path.Combine(path, $"{SolutionName}.sln"))) - path = path.Remove(path.LastIndexOf(Path.DirectorySeparatorChar)); - path += Path.DirectorySeparatorChar; - - Environment.CurrentDirectory = path; - } - - private static bool runCommand(string command, string args) - { - var psi = new ProcessStartInfo(command, args) - { - WorkingDirectory = solutionPath, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - WindowStyle = ProcessWindowStyle.Hidden - }; - - Process p = Process.Start(psi); - if (p == null) return false; - - string output = p.StandardOutput.ReadToEnd(); - output += p.StandardError.ReadToEnd(); - - if (p.ExitCode == 0) return true; - - write(output); - error($"Command {command} {args} failed!"); - return false; - } - - private static string readLineMasked() - { - var fg = Console.ForegroundColor; - Console.ForegroundColor = Console.BackgroundColor; - var ret = Console.ReadLine(); - Console.ForegroundColor = fg; - - return ret; - } - - private static void error(string message) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"FATAL ERROR: {message}"); - - pauseIfInteractive(); - Environment.Exit(-1); - } - - private static void pauseIfInteractive() - { - if (interactive) - Console.ReadLine(); - else - Console.WriteLine(); - } - - private static bool updateAppveyorVersion(string version) - { - try - { - using (PowerShell ps = PowerShell.Create()) - { - ps.AddScript($"Update-AppveyorBuild -Version \"{version}\""); - ps.Invoke(); - } - return true; - } - catch - { - // we don't have appveyor and don't care - } - - return false; - } - - private static void write(string message, ConsoleColor col = ConsoleColor.Gray) - { - if (sw.ElapsedMilliseconds > 0) - { - Console.ForegroundColor = ConsoleColor.Green; - Console.Write(sw.ElapsedMilliseconds.ToString().PadRight(8)); - } - Console.ForegroundColor = col; - Console.WriteLine(message); - } - - public static void AuthenticatedBlockingPerform(this WebRequest r) - { - r.AddHeader("Authorization", $"token {GitHubAccessToken}"); - r.Perform(); - } - } - - internal class RawFileWebRequest : WebRequest - { - public RawFileWebRequest(string url) : base(url) - { - } - - protected override string Accept => "application/octet-stream"; - } - - internal class ReleaseLine - { - public string Hash; - public string Filename; - public int Filesize; - - public ReleaseLine(string line) - { - var split = line.Split(' '); - Hash = split[0]; - Filename = split[1]; - Filesize = int.Parse(split[2]); - } - - public override string ToString() => $"{Hash} {Filename} {Filesize}"; - } -} diff --git a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj b/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj deleted file mode 100644 index 063fb89918..0000000000 --- a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net471 - Exe - AnyCPU - true - - - - - - - - - - - - \ No newline at end of file diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index a495d7048d..844db4a80f 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -12,6 +12,7 @@ using osu.Framework.Platform; using osu.Game; using OpenTK.Input; using Microsoft.Win32; +using osu.Framework.Platform.Windows; namespace osu.Desktop { @@ -40,7 +41,7 @@ namespace osu.Desktop /// /// A method of accessing an osu-stable install in a controlled fashion. /// - private class StableStorage : DesktopStorage + private class StableStorage : WindowsStorage { protected override string LocateBasePath() { diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs index b984c0bbba..bc1faec822 100644 --- a/osu.Desktop/Overlays/VersionManager.cs +++ b/osu.Desktop/Overlays/VersionManager.cs @@ -1,19 +1,20 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Diagnostics; +using System; using osu.Framework.Allocation; -using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.Platform; 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 osu.Game.Utils; using OpenTK; using OpenTK.Graphics; @@ -24,16 +25,18 @@ namespace osu.Desktop.Overlays private OsuConfigManager config; private OsuGameBase game; private NotificationOverlay notificationOverlay; + private GameHost host; public override bool HandleKeyboardInput => false; public override bool HandleMouseInput => false; [BackgroundDependencyLoader] - private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game, OsuConfigManager config) + private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game, OsuConfigManager config, GameHost host) { notificationOverlay = notification; this.config = config; this.game = game; + this.host = host; AutoSizeAxes = Axes.Both; Anchor = Anchor.BottomCentre; @@ -106,19 +109,19 @@ namespace osu.Desktop.Overlays // only show a notification if we've previously saved a version to the config file (ie. not the first run). if (!string.IsNullOrEmpty(lastVersion)) - notificationOverlay.Post(new UpdateCompleteNotification(version)); + notificationOverlay.Post(new UpdateCompleteNotification(version, host.OpenUrlExternally)); } } private class UpdateCompleteNotification : SimpleNotification { - public UpdateCompleteNotification(string version) + public UpdateCompleteNotification(string version, Action openUrl = null) { Text = $"You are now running osu!lazer {version}.\nClick to see what's new!"; Icon = FontAwesome.fa_check_square; Activated = delegate { - Process.Start($"https://github.com/ppy/osu/releases/tag/v{version}"); + openUrl?.Invoke($"https://osu.ppy.sh/home/changelog/lazer/{version}"); return true; }; } diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 3d64cab84e..3cf95e9b3e 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -1,7 +1,7 @@ - + - net471;netcoreapp2.0 + net471;netcoreapp2.1 WinExe AnyCPU true @@ -20,20 +20,19 @@ osu.Desktop.Program - - + - + - + \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json index eb80f4474c..2a82d65014 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json @@ -22,7 +22,7 @@ }, "type": "mono", "request": "launch", - "program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Catch.Tests.exe", + "program": "${workspaceRoot}/bin/Release/net471/osu.Game.Rulesets.Catch.Tests.exe", "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release, msbuild)", "runtimeExecutable": null, @@ -30,12 +30,12 @@ "console": "internalConsole" }, { - "name": "VisualTests (Debug, netcoreapp2.0)", + "name": "VisualTests (Debug, netcoreapp2.1)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Catch.Tests.dll" + "${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Catch.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug, dotnet)", @@ -43,12 +43,12 @@ "console": "internalConsole" }, { - "name": "VisualTests (Release, netcoreapp2.0)", + "name": "VisualTests (Release, netcoreapp2.1)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Catch.Tests.dll" + "${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Catch.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release, dotnet)", diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json index 41ae88f425..6c6d562512 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json @@ -40,7 +40,7 @@ "build", "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:GenerateFullPaths=true", "/m", "/verbosity:m" @@ -56,7 +56,7 @@ "build", "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:Configuration=Release", "/p:GenerateFullPaths=true", "/m", @@ -75,7 +75,7 @@ "problemMatcher": [] }, { - "label": "Restore (netcoreapp2.0)", + "label": "Restore (netcoreapp2.1)", "type": "shell", "command": "dotnet", "args": [ diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs index 097750d7e0..34e07170bd 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs @@ -53,10 +53,10 @@ namespace osu.Game.Rulesets.Catch.Tests return beatmap; } - protected override Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) + protected override Player CreatePlayer(Ruleset ruleset) { - beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); - return base.CreatePlayer(beatmap, ruleset); + Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); + return base.CreatePlayer(ruleset); } } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseCatcherArea.cs index f239290ed4..5119260c53 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseCatcherArea.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Tests Child = catcherArea = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size }) { Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomLeft + Origin = Anchor.TopLeft }, }; } diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseFruitObjects.cs index 275752523d..e77dd76353 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestCaseFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestCaseFruitObjects.cs @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Catch.Tests typeof(DrawableCatchHitObject), typeof(DrawableFruit), typeof(DrawableDroplet), + typeof(BananaShower), typeof(Pulp), }; @@ -53,12 +54,19 @@ namespace osu.Game.Rulesets.Catch.Tests private DrawableFruit createDrawable(int index) { - var fruit = new Fruit - { - StartTime = 1000000000000, - IndexInBeatmap = index, - Scale = 1.5f, - }; + Fruit fruit = index == 5 + ? new BananaShower.Banana + { + StartTime = 1000000000000, + IndexInBeatmap = index, + Scale = 1.5f, + } + : new Fruit + { + StartTime = 1000000000000, + IndexInBeatmap = index, + Scale = 1.5f, + }; return new DrawableFruit(fruit) { diff --git a/osu.Game.Rulesets.Catch.Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Catch.Tests/TestCasePerformancePoints.cs deleted file mode 100644 index 9512cf2061..0000000000 --- a/osu.Game.Rulesets.Catch.Tests/TestCasePerformancePoints.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; - -namespace osu.Game.Rulesets.Catch.Tests -{ - [TestFixture] - public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints - { - public TestCasePerformancePoints() - : base(new CatchRuleset()) - { - } - } -} diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 3797edde61..93fa2c4d67 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp2.0;net471 + netcoreapp2.1;net471 diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index f40ef67dfb..ad500606ed 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -28,7 +28,20 @@ namespace osu.Game.Rulesets.Catch.Beatmaps var endTime = obj as IHasEndTime; if (positionData == null) + { + if (endTime != null) + { + yield return new BananaShower + { + StartTime = obj.StartTime, + Samples = obj.Samples, + Duration = endTime.Duration, + NewCombo = comboData?.NewCombo ?? false + }; + } + yield break; + } if (curveData != null) { @@ -48,19 +61,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps yield break; } - if (endTime != null) - { - yield return new BananaShower - { - StartTime = obj.StartTime, - Samples = obj.Samples, - Duration = endTime.Duration, - NewCombo = comboData?.NewCombo ?? false - }; - - yield break; - } - yield return new Fruit { StartTime = obj.StartTime, diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 2325a8cad9..fc6e23c884 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -82,56 +82,25 @@ namespace osu.Game.Rulesets.Catch { new CatchModEasy(), new CatchModNoFail(), - new MultiMod - { - Mods = new Mod[] - { - new CatchModHalfTime(), - new CatchModDaycore(), - }, - }, + new MultiMod(new CatchModHalfTime(), new CatchModDaycore()) }; - case ModType.DifficultyIncrease: return new Mod[] { new CatchModHardRock(), - new MultiMod - { - Mods = new Mod[] - { - new CatchModSuddenDeath(), - new CatchModPerfect(), - }, - }, - new MultiMod - { - Mods = new Mod[] - { - new CatchModDoubleTime(), - new CatchModNightcore(), - }, - }, + new MultiMod(new CatchModSuddenDeath(), new CatchModPerfect()), + new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()), new CatchModHidden(), new CatchModFlashlight(), }; - case ModType.Special: return new Mod[] { new CatchModRelax(), null, null, - new MultiMod - { - Mods = new Mod[] - { - new CatchModAutoplay(), - new ModCinema(), - }, - }, + new MultiMod(new CatchModAutoplay(), new ModCinema()), }; - default: return new Mod[] { }; } @@ -143,7 +112,7 @@ namespace osu.Game.Rulesets.Catch public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o }; - public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap); + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap); public override int? LegacyID => 2; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index f8351b7519..562374087e 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -1,18 +1,19 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Difficulty { public class CatchDifficultyCalculator : DifficultyCalculator { - public CatchDifficultyCalculator(IBeatmap beatmap) : base(beatmap) + public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { } - public override double Calculate(Dictionary categoryDifficulty = null) => 0; + protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) => new DifficultyAttributes(mods, 0); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index 3dbda708e5..e3564b5967 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { public virtual bool CanBePlated => false; + public virtual bool StaysOnPlate => CanBePlated; + protected DrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs index a19d67ebbe..5c8a7c4a7c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs @@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { private Pulp pulp; + public override bool StaysOnPlate => false; + public DrawableDroplet(Droplet h) : base(h) { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs index 41792b10a4..4603148114 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs @@ -18,12 +18,19 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { private Circle border; + private const float drawable_radius = (float)CatchHitObject.OBJECT_RADIUS * radius_adjust; + + /// + /// Because we're adding a border around the fruit, we need to scale down some. + /// + private const float radius_adjust = 1.1f; + public DrawableFruit(Fruit h) : base(h) { Origin = Anchor.Centre; - Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS); + Size = new Vector2(drawable_radius); Masking = false; Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; @@ -44,14 +51,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { Hollow = !HitObject.HyperDash, Type = EdgeEffectType.Glow, - Radius = 4, + Radius = 4 * radius_adjust, Colour = HitObject.HyperDash ? Color4.Red : AccentColour.Darken(1).Opacity(0.6f) }, - Size = new Vector2(Height * 1.5f), + Size = new Vector2(Height), Anchor = Anchor.Centre, Origin = Anchor.Centre, BorderColour = Color4.White, - BorderThickness = 4f, + BorderThickness = 3f * radius_adjust, Children = new Framework.Graphics.Drawable[] { new Box @@ -82,8 +89,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable private Framework.Graphics.Drawable createPulp(FruitVisualRepresentation representation) { - const float large_pulp_3 = 13f; - const float distance_from_centre_3 = 0.23f; + const float large_pulp_3 = 8f * radius_adjust; + const float distance_from_centre_3 = 0.15f; const float large_pulp_4 = large_pulp_3 * 0.925f; const float distance_from_centre_4 = distance_from_centre_3 / 0.925f; @@ -106,11 +113,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, AccentColour = AccentColour, Size = new Vector2(small_pulp), - Y = 0.05f, + Y = -0.34f, }, new Pulp { @@ -146,11 +151,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, AccentColour = AccentColour, Size = new Vector2(small_pulp), - Y = 0.1f, + Y = -0.3f, }, new Pulp { @@ -186,11 +189,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, AccentColour = AccentColour, Size = new Vector2(small_pulp), - Y = -0.1f, + Y = -0.33f, }, new Pulp { @@ -220,10 +221,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, AccentColour = AccentColour, Size = new Vector2(small_pulp), + Y = -0.25f, }, new Pulp { @@ -253,16 +253,15 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { new Pulp { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, AccentColour = AccentColour, Size = new Vector2(small_pulp), - Y = -0.15f + Y = -0.3f }, new Pulp { AccentColour = AccentColour, - Size = new Vector2(large_pulp_4 * 1.2f, large_pulp_4 * 3), + Size = new Vector2(large_pulp_4 * 0.8f, large_pulp_4 * 2.5f), + Y = 0.05f, }, } }; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs index d17a72a165..250dc8c7f1 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs @@ -29,14 +29,24 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces set { accentColour = value; - - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 8, - Colour = accentColour.Darken(0.2f).Opacity(0.75f) - }; + if (IsLoaded) updateAccentColour(); } } + + private void updateAccentColour() + { + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = Size.X / 2, + Colour = accentColour.Darken(0.2f).Opacity(0.75f) + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateAccentColour(); + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index ae799875a9..b2d8e3f8a5 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -124,6 +124,9 @@ namespace osu.Game.Rulesets.Catch.Objects X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH }); } + + if (NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested) + lastNested.LastInCombo = LastInCombo; } public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 070dc19a6f..52763e09af 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.UI public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); + protected override Vector2 PlayfieldArea => new Vector2(0.86f); // matches stable's vertical offset for catcher plate + protected override DrawableHitObject GetVisualRepresentation(CatchHitObject h) { switch (h) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 181536a91e..b62e9997d4 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI { public class CatcherArea : Container { - public const float CATCHER_SIZE = 172; + public const float CATCHER_SIZE = 84; protected readonly Catcher MovableCatcher; @@ -48,6 +48,16 @@ namespace osu.Game.Rulesets.Catch.UI public void OnJudgement(DrawableCatchHitObject fruit, Judgement judgement) { + void runAfterLoaded(Action action) + { + // this is required to make this run after the last caught fruit runs UpdateState at least once. + // TODO: find a better alternative + if (lastPlateableFruit.IsLoaded) + action(); + else + lastPlateableFruit.OnLoadComplete = _ => action(); + } + if (judgement.IsHit && fruit.CanBePlated) { var caughtFruit = (DrawableCatchHitObject)GetVisualRepresentation?.Invoke(fruit.HitObject); @@ -63,21 +73,17 @@ namespace osu.Game.Rulesets.Catch.UI caughtFruit.LifetimeEnd = double.MaxValue; MovableCatcher.Add(caughtFruit); - lastPlateableFruit = caughtFruit; + + if (!fruit.StaysOnPlate) + runAfterLoaded(() => MovableCatcher.Explode(caughtFruit)); + } if (fruit.HitObject.LastInCombo) { if (judgement.IsHit) - { - // this is required to make this run after the last caught fruit runs UpdateState at least once. - // TODO: find a better alternative - if (lastPlateableFruit.IsLoaded) - MovableCatcher.Explode(); - else - lastPlateableFruit.OnLoadComplete = _ => { MovableCatcher.Explode(); }; - } + runAfterLoaded(() => MovableCatcher.Explode()); else MovableCatcher.Drop(); } @@ -99,8 +105,6 @@ namespace osu.Game.Rulesets.Catch.UI public class Catcher : Container, IKeyBindingHandler { - private Texture texture; - private Container caughtFruit; public Container ExplodingFruitTarget; @@ -121,10 +125,8 @@ namespace osu.Game.Rulesets.Catch.UI } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load() { - texture = textures.Get(@"Play/Catch/fruit-catcher-idle"); - Children = new Drawable[] { caughtFruit = new Container @@ -196,13 +198,7 @@ namespace osu.Game.Rulesets.Catch.UI Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); } - private Sprite createCatcherSprite() => new Sprite - { - Size = new Vector2(CATCHER_SIZE), - FillMode = FillMode.Fill, - Texture = texture, - OriginPosition = new Vector2(-3, 10) // temporary until the sprite is aligned correctly. - }; + private Sprite createCatcherSprite() => new CatcherSprite(); /// /// Add a caught fruit to the catcher's stack. @@ -388,27 +384,47 @@ namespace osu.Game.Rulesets.Catch.UI var fruit = caughtFruit.ToArray(); foreach (var f in fruit) + Explode(f); + } + + public void Explode(DrawableHitObject fruit) + { + var originalX = fruit.X * Scale.X; + + if (ExplodingFruitTarget != null) { - var originalX = f.X * Scale.X; + fruit.Anchor = Anchor.TopLeft; + fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); - if (ExplodingFruitTarget != null) - { - f.Anchor = Anchor.TopLeft; - f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget); + caughtFruit.Remove(fruit); - caughtFruit.Remove(f); + ExplodingFruitTarget.Add(fruit); + } - ExplodingFruitTarget.Add(f); - } + fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine) + .Then() + .MoveToY(fruit.Y + 50, 500, Easing.InSine); - f.MoveToY(f.Y - 50, 250, Easing.OutSine) - .Then() - .MoveToY(f.Y + 50, 500, Easing.InSine); + fruit.MoveToX(fruit.X + originalX * 6, 1000); + fruit.FadeOut(750); - f.MoveToX(f.X + originalX * 6, 1000); - f.FadeOut(750); + fruit.Expire(); + } - f.Expire(); + private class CatcherSprite : Sprite + { + public CatcherSprite() + { + Size = new Vector2(CATCHER_SIZE); + + // Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling. + OriginPosition = new Vector2(-0.02f, 0.06f) * CATCHER_SIZE; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get(@"Play/Catch/fruit-catcher-idle"); } } } diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json index fceb403f30..bc41d4ccf9 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json @@ -22,7 +22,7 @@ }, "type": "mono", "request": "launch", - "program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Mania.Tests.exe", + "program": "${workspaceRoot}/bin/Release/net471/osu.Game.Rulesets.Mania.Tests.exe", "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release, msbuild)", "runtimeExecutable": null, @@ -30,12 +30,12 @@ "console": "internalConsole" }, { - "name": "VisualTests (Debug, netcoreapp2.0)", + "name": "VisualTests (Debug, netcoreapp2.1)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Mania.Tests.dll" + "${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Mania.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug, dotnet)", @@ -43,12 +43,12 @@ "console": "internalConsole" }, { - "name": "VisualTests (Release, netcoreapp2.0)", + "name": "VisualTests (Release, netcoreapp2.1)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Mania.Tests.dll" + "${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Mania.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release, dotnet)", diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json index b04b068b0d..7fc2f7b2ef 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json @@ -40,7 +40,7 @@ "build", "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:GenerateFullPaths=true", "/m", "/verbosity:m" @@ -56,7 +56,7 @@ "build", "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:Configuration=Release", "/p:GenerateFullPaths=true", "/m", @@ -75,7 +75,7 @@ "problemMatcher": [] }, { - "label": "Restore (netcoreapp2.0)", + "label": "Restore (netcoreapp2.1)", "type": "shell", "command": "dotnet", "args": [ diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs index dff2b2d56a..b064d82a23 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseManiaPlayfield.cs @@ -98,17 +98,12 @@ namespace osu.Game.Rulesets.Mania.Tests }); } - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); - [BackgroundDependencyLoader] private void load(RulesetStore rulesets, SettingsStore settings) { maniaRuleset = rulesets.GetRuleset(3); - dependencies.Cache(new ManiaConfigManager(settings, maniaRuleset, 4)); + Dependencies.Cache(new ManiaConfigManager(settings, maniaRuleset, 4)); } private ManiaPlayfield createPlayfield(int cols, bool inverted = false) diff --git a/osu.Game.Rulesets.Mania.Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Mania.Tests/TestCasePerformancePoints.cs deleted file mode 100644 index c15a6dd688..0000000000 --- a/osu.Game.Rulesets.Mania.Tests/TestCasePerformancePoints.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; - -namespace osu.Game.Rulesets.Mania.Tests -{ - [TestFixture] - public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints - { - public TestCasePerformancePoints() - : base(new ManiaRuleset()) - { - } - } -} diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index e90155568e..77504fdc3c 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp2.0;net471 + netcoreapp2.1;net471 diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index afa9bdbbd7..f60958d581 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -58,6 +58,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy public override Pattern Generate() { + if (TotalColumns == 1) + { + var pattern = new Pattern(); + addToPattern(pattern, 0, HitObject.StartTime, endTime); + return pattern; + } + if (spanCount > 1) { if (segmentDuration <= 90) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index cec3e18ad6..b4160dc98b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -77,10 +77,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } else convertType |= PatternType.LowProbability; + + if ((convertType & PatternType.KeepSingle) == 0) + { + if (HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_FINISH) && TotalColumns != 8) + convertType |= PatternType.Mirror; + else + convertType |= PatternType.Gathered; + } } public override Pattern Generate() { + if (TotalColumns == 1) + { + var pattern = new Pattern(); + addToPattern(pattern, 0); + return pattern; + } + int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; if ((convertType & PatternType.Reverse) > 0 && PreviousPattern.HitObjects.Any()) @@ -346,7 +361,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy addToCentre = false; if ((convertType & PatternType.ForceNotStack) > 0) - return getRandomNoteCount(p2 / 2, p2, (p2 + p3) / 2, p3); + return getRandomNoteCount(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); switch (TotalColumns) { diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaConfigManager.cs index ea5f590bd1..d9e360081d 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaConfigManager.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mania.Configuration { public class ManiaConfigManager : RulesetConfigManager { - public ManiaConfigManager(SettingsStore settings, RulesetInfo ruleset, int variant) + public ManiaConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null) : base(settings, ruleset, variant) { } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 2517839355..5fa113224d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; @@ -28,47 +29,36 @@ namespace osu.Game.Rulesets.Mania.Difficulty /// private const double decay_weight = 0.9; - /// - /// HitObjects are stored as a member variable. - /// - private readonly List difficultyHitObjects = new List(); + private readonly bool isForCurrentRuleset; - public ManiaDifficultyCalculator(IBeatmap beatmap) - : base(beatmap) + public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { + isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); } - public ManiaDifficultyCalculator(IBeatmap beatmap, Mod[] mods) - : base(beatmap, mods) + protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) { - } + var difficultyHitObjects = new List(); - public override double Calculate(Dictionary categoryDifficulty = null) - { - // Fill our custom DifficultyHitObject class, that carries additional information - difficultyHitObjects.Clear(); - - int columnCount = (Beatmap as ManiaBeatmap)?.TotalColumns ?? 7; + int columnCount = ((ManiaBeatmap)beatmap).TotalColumns; // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. // Note: Stable sort is done so that the ordering of hitobjects with equal start times doesn't change - difficultyHitObjects.AddRange(Beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime)); + difficultyHitObjects.AddRange(beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime)); - if (!calculateStrainValues()) - return 0; + if (!calculateStrainValues(difficultyHitObjects, timeRate)) + return new DifficultyAttributes(mods, 0); - double starRating = calculateDifficulty() * star_scaling_factor; + double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor; - if (categoryDifficulty != null) - categoryDifficulty["Strain"] = starRating; - - return starRating; + return new DifficultyAttributes(mods, starRating); } - private bool calculateStrainValues() + private bool calculateStrainValues(List objects, double timeRate) { // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. - using (List.Enumerator hitObjectsEnumerator = difficultyHitObjects.GetEnumerator()) + using (var hitObjectsEnumerator = objects.GetEnumerator()) { if (!hitObjectsEnumerator.MoveNext()) return false; @@ -79,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty while (hitObjectsEnumerator.MoveNext()) { var next = hitObjectsEnumerator.Current; - next?.CalculateStrains(current, TimeRate); + next?.CalculateStrains(current, timeRate); current = next; } @@ -87,9 +77,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } - private double calculateDifficulty() + private double calculateDifficulty(List objects, double timeRate) { - double actualStrainStep = strain_step * TimeRate; + double actualStrainStep = strain_step * timeRate; // Find the highest strain value within each strain step List highestStrains = new List(); @@ -97,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval ManiaHitObjectDifficulty previousHitObject = null; - foreach (var hitObject in difficultyHitObjects) + foreach (var hitObject in objects) { // While we are beyond the current interval push the currently available maximum to our strain list while (hitObject.BaseHitObject.StartTime > intervalEndTime) @@ -141,5 +131,36 @@ namespace osu.Game.Rulesets.Mania.Difficulty return difficulty; } + + protected override Mod[] DifficultyAdjustmentMods + { + get + { + var mods = new Mod[] + { + new ManiaModDoubleTime(), + new ManiaModHalfTime(), + new ManiaModEasy(), + new ManiaModHardRock(), + }; + + if (isForCurrentRuleset) + return mods; + + // if we are a convert, we can be played in any key mod. + return mods.Concat(new Mod[] + { + new ManiaModKey1(), + new ManiaModKey2(), + new ManiaModKey3(), + new ManiaModKey4(), + new ManiaModKey5(), + new ManiaModKey6(), + new ManiaModKey7(), + new ManiaModKey8(), + new ManiaModKey9(), + }).ToArray(); + } + } } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index e6e3028d62..b6089b830b 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty private int countMeh; private int countMiss; - public ManiaPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) + public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score) : base(ruleset, beatmap, score) { } @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeStrainValue() { // Obtain strain difficulty - double strainValue = Math.Pow(5 * Math.Max(1, Attributes["Strain"] / 0.2) - 4.0, 2.2) / 135.0; + double strainValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0; // Longer maps are worth more strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); @@ -105,7 +105,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeAccuracyValue(double strainValue) { - double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future + double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate; if (hitWindowGreat <= 0) return 0; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 02ecb3afda..ac5fbfdde0 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -15,8 +15,11 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Beatmaps.Legacy; +using osu.Game.Configuration; +using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; using osu.Game.Rulesets.Scoring; @@ -26,7 +29,7 @@ namespace osu.Game.Rulesets.Mania { public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new ManiaRulesetContainer(this, beatmap); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score); public override IEnumerable ConvertLegacyMods(LegacyMods mods) { @@ -105,78 +108,34 @@ namespace osu.Game.Rulesets.Mania { new ManiaModEasy(), new ManiaModNoFail(), - new MultiMod - { - Mods = new Mod[] - { - new ManiaModHalfTime(), - new ManiaModDaycore(), - }, - }, + new MultiMod(new ManiaModHalfTime(), new ManiaModDaycore()), }; - case ModType.DifficultyIncrease: return new Mod[] { new ManiaModHardRock(), - new MultiMod - { - Mods = new Mod[] - { - new ManiaModSuddenDeath(), - new ManiaModPerfect(), - }, - }, - new MultiMod - { - Mods = new Mod[] - { - new ManiaModDoubleTime(), - new ManiaModNightcore(), - }, - }, - new MultiMod - { - Mods = new Mod[] - { - new ManiaModFadeIn(), - new ManiaModHidden(), - } - }, + new MultiMod(new ManiaModSuddenDeath(), new ManiaModPerfect()), + new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()), + new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()), new ManiaModFlashlight(), }; - case ModType.Special: return new Mod[] { - new MultiMod - { - Mods = new Mod[] - { - new ManiaModKey4(), - new ManiaModKey5(), - new ManiaModKey6(), - new ManiaModKey7(), - new ManiaModKey8(), - new ManiaModKey9(), - new ManiaModKey1(), - new ManiaModKey2(), - new ManiaModKey3(), - }, - }, + new MultiMod(new ManiaModKey4(), + new ManiaModKey5(), + new ManiaModKey6(), + new ManiaModKey7(), + new ManiaModKey8(), + new ManiaModKey9(), + new ManiaModKey1(), + new ManiaModKey2(), + new ManiaModKey3()), new ManiaModRandom(), new ManiaModDualStages(), new ManiaModMirror(), - new MultiMod - { - Mods = new Mod[] - { - new ManiaModAutoplay(), - new ModCinema(), - }, - }, + new MultiMod(new ManiaModAutoplay(), new ModCinema()), }; - default: return new Mod[] { }; } @@ -188,12 +147,14 @@ namespace osu.Game.Rulesets.Mania public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o }; - public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods); + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(this, beatmap); public override int? LegacyID => 3; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); + public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaConfigManager(settings, RulesetInfo); + public ManiaRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) { diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs index e02db68a28..6bfe295c3d 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mods; @@ -24,5 +26,18 @@ namespace osu.Game.Rulesets.Mania.Mods mbc.TargetColumns = KeyCount; } + + public override Type[] IncompatibleMods => new[] + { + typeof(ManiaModKey1), + typeof(ManiaModKey2), + typeof(ManiaModKey3), + typeof(ManiaModKey4), + typeof(ManiaModKey5), + typeof(ManiaModKey6), + typeof(ManiaModKey7), + typeof(ManiaModKey8), + typeof(ManiaModKey9), + }.Except(new[] { GetType() }).ToArray(); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs index 7f3985b26d..256811b4c1 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Name => "Dual Stages"; public override string ShortenedName => "DS"; public override string Description => @"Double the stages, double the fun!"; - public override double ScoreMultiplier => 0; + public override double ScoreMultiplier => 1; private bool isForCurrentRuleset; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs index 5af898287a..2f951461c3 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string ShortenedName => "RD"; public override FontAwesome Icon => FontAwesome.fa_osu_dice; public override string Description => @"Shuffle around the keys!"; - public override double ScoreMultiplier => 0; + public override double ScoreMultiplier => 1; public void ApplyToRulesetContainer(RulesetContainer rulesetContainer) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 2147c5a761..d0fc6aa3d6 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -5,6 +5,7 @@ using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects.Drawables; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables : base(barLine) { RelativeSizeAxes = Axes.X; - Height = 1; + Height = 2f; AddInternal(new Box { @@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Both, + Colour = new Color4(255, 204, 33, 255), }); bool isMajor = barLine.BeatIndex % (int)barLine.ControlPoint.TimeSignature == 0; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 8791e8ed86..e008fa952e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -2,7 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; -using osu.Game.Rulesets.Objects.Drawables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using OpenTK.Graphics; @@ -18,12 +18,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableHoldNote : DrawableManiaHitObject, IKeyBindingHandler { + public override bool DisplayJudgement => false; + private readonly DrawableNote head; private readonly DrawableNote tail; - private readonly GlowPiece glowPiece; private readonly BodyPiece bodyPiece; - private readonly Container fullHeightContainer; /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. @@ -35,25 +35,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// private bool hasBroken; + private readonly Container tickContainer; + public DrawableHoldNote(HoldNote hitObject, ManiaAction action) : base(hitObject, action) { - Container tickContainer; RelativeSizeAxes = Axes.X; InternalChildren = new Drawable[] { - // The hit object itself cannot be used for various elements because the tail overshoots it - // So a specialized container that is updated to contain the tail height is used - fullHeightContainer = new Container - { - RelativeSizeAxes = Axes.X, - Child = glowPiece = new GlowPiece() - }, bodyPiece = new BodyPiece { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, }, tickContainer = new Container @@ -90,21 +82,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.AccentColour = value; - glowPiece.AccentColour = value; bodyPiece.AccentColour = value; head.AccentColour = value; tail.AccentColour = value; - } - } - - protected override void UpdateState(ArmedState state) - { - switch (state) - { - case ArmedState.Hit: - // Good enough for now, we just want them to have a lifetime end - this.Delay(2000).Expire(); - break; + tickContainer.ForEach(t => t.AccentColour = value); } } @@ -119,12 +100,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.Update(); // Make the body piece not lie under the head note - bodyPiece.Y = head.Height; - bodyPiece.Height = DrawHeight - head.Height; - - // Make the fullHeightContainer "contain" the height of the tail note, keeping in mind - // that the tail note overshoots the height of this hit object - fullHeightContainer.Height = DrawHeight + tail.Height; + bodyPiece.Y = head.Height / 2; + bodyPiece.Height = DrawHeight - head.Height / 2 + tail.Height / 2; } public bool OnPressed(ManiaAction action) @@ -173,8 +150,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables : base(holdNote.HitObject.Head, action) { this.holdNote = holdNote; - - GlowPiece.Alpha = 0; } public override bool OnPressed(ManiaAction action) @@ -192,11 +167,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return true; } - - protected override void UpdateState(ArmedState state) - { - // The holdnote keeps scrolling through for now, so having the head disappear looks weird - } } /// @@ -217,8 +187,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables : base(holdNote.HitObject.Tail, action) { this.holdNote = holdNote; - - GlowPiece.Alpha = 0; } protected override void CheckForJudgements(bool userTriggered, double timeOffset) @@ -251,11 +219,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }); } - protected override void UpdateState(ArmedState state) - { - // The holdnote keeps scrolling through, so having the tail disappear looks weird - } - public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down public override bool OnReleased(ManiaAction action) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index 74c17ad0fb..5df6079efa 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -8,7 +8,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Judgements; -using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Scoring; @@ -87,16 +86,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables AddJudgement(new HoldNoteTickJudgement { Result = HitResult.Perfect }); } - protected override void UpdateState(ArmedState state) - { - switch (State.Value) - { - case ArmedState.Hit: - AccentColour = Color4.Green; - break; - } - } - protected override void Update() { if (AllJudged) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index db1fec40de..fbf1ee8460 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -27,5 +27,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != null) Action = action.Value; } + + protected override void UpdateState(ArmedState state) + { + switch (state) + { + case ArmedState.Miss: + this.FadeOut(150, Easing.In).Expire(); + break; + case ArmedState.Hit: + this.FadeOut(150, Easing.OutQuint).Expire(); + break; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 0340e6bba3..3de0a9c5cc 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -1,12 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Extensions.Color4Extensions; using OpenTK.Graphics; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables @@ -16,9 +17,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableNote : DrawableManiaHitObject, IKeyBindingHandler { - protected readonly GlowPiece GlowPiece; - - private readonly LaneGlowPiece laneGlowPiece; private readonly NotePiece headPiece; public DrawableNote(Note hitObject, ManiaAction action) @@ -27,14 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + CornerRadius = 5; + Masking = true; + InternalChildren = new Drawable[] { - laneGlowPiece = new LaneGlowPiece - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - GlowPiece = new GlowPiece(), headPiece = new NotePiece { Anchor = Anchor.TopCentre, @@ -49,9 +44,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables set { base.AccentColour = value; - laneGlowPiece.AccentColour = AccentColour; - GlowPiece.AccentColour = AccentColour; headPiece.AccentColour = AccentColour; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = AccentColour.Lighten(1f).Opacity(0.6f), + Radius = 10, + }; } } @@ -71,17 +71,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables AddJudgement(new ManiaJudgement { Result = result }); } - protected override void UpdateState(ArmedState state) - { - switch (state) - { - case ArmedState.Hit: - case ArmedState.Miss: - this.FadeOut(100).Expire(); - break; - } - } - public virtual bool OnPressed(ManiaAction action) { if (action != Action) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs index 17644a78a5..4ab2da208a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/BodyPiece.cs @@ -123,8 +123,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces if (!IsLoaded) return; - foreground.Colour = AccentColour.Opacity(0.4f); - background.Colour = AccentColour.Opacity(0.2f); + foreground.Colour = AccentColour.Opacity(0.9f); + background.Colour = AccentColour.Opacity(0.6f); subtractionCache.Invalidate(); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs index e23b24508b..9ebeb91e0c 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces /// internal class NotePiece : Container, IHasAccentColour { - private const float head_height = 10; + public const float NOTE_HEIGHT = 10; private const float head_colour_height = 6; private readonly Box colouredBox; @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces public NotePiece() { RelativeSizeAxes = Axes.X; - Height = head_height; + Height = NOTE_HEIGHT; Children = new[] { diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 52d514221d..9c1830e642 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Mania.UI public ManiaAction Action; private readonly Box background; + private readonly Box backgroundOverlay; private readonly Container hitTargetBar; private readonly Container keyIcon; @@ -42,22 +43,32 @@ namespace osu.Game.Rulesets.Mania.UI protected override Container Content => content; private readonly Container content; - private const float opacity_released = 0.1f; - private const float opacity_pressed = 0.25f; - public Column() : base(ScrollingDirection.Up) { RelativeSizeAxes = Axes.Y; Width = column_width; + Masking = true; + CornerRadius = 5; + InternalChildren = new Drawable[] { background = new Box { Name = "Background", RelativeSizeAxes = Axes.Both, - Alpha = opacity_released + Alpha = 0.3f + }, + backgroundOverlay = new Box + { + Name = "Background Gradient Overlay", + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Blending = BlendingMode.Additive, + Alpha = 0 }, new Container { @@ -182,6 +193,7 @@ namespace osu.Game.Rulesets.Mania.UI accentColour = value; background.Colour = accentColour; + backgroundOverlay.Colour = ColourInfo.GradientVertical(accentColour.Opacity(0.6f), accentColour.Opacity(0)); hitTargetBar.EdgeEffect = new EdgeEffectParameters { @@ -213,7 +225,7 @@ namespace osu.Game.Rulesets.Mania.UI internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) { - if (!judgement.IsHit) + if (!judgement.IsHit || !judgedObject.DisplayJudgement) return; explosionContainer.Add(new HitExplosion(judgedObject)); @@ -223,8 +235,8 @@ namespace osu.Game.Rulesets.Mania.UI { if (action == Action) { - background.FadeTo(opacity_pressed, 50, Easing.OutQuint); - keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint); + backgroundOverlay.FadeTo(1, 50, Easing.OutQuint).Then().FadeTo(0.5f, 250, Easing.OutQuint); + keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint).Then().ScaleTo(1.3f, 250, Easing.OutQuint); } return false; @@ -234,8 +246,8 @@ namespace osu.Game.Rulesets.Mania.UI { if (action == Action) { - background.FadeTo(opacity_released, 800, Easing.OutQuart); - keyIcon.ScaleTo(1f, 400, Easing.OutQuart); + backgroundOverlay.FadeTo(0, 250, Easing.OutQuint); + keyIcon.ScaleTo(1f, 125, Easing.OutQuint); } return false; @@ -265,8 +277,13 @@ namespace osu.Game.Rulesets.Mania.UI if (action != Action) return false; - var hitObject = HitObjects.Objects.LastOrDefault(h => h.HitObject.StartTime > Time.Current) ?? HitObjects.Objects.FirstOrDefault(); - hitObject?.PlaySamples(); + var nextObject = + HitObjects.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ?? + // fallback to non-alive objects to find next off-screen object + HitObjects.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ?? + HitObjects.Objects.LastOrDefault(); + + nextObject?.PlaySamples(); return true; } diff --git a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs index f01dfc0db1..bf8db63137 100644 --- a/osu.Game.Rulesets.Mania/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/HitExplosion.cs @@ -1,19 +1,23 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.MathUtils; using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; namespace osu.Game.Rulesets.Mania.UI { internal class HitExplosion : CompositeDrawable { - private readonly Box inner; + public override bool RemoveWhenNotAlive => true; + + private readonly CircularContainer circle; public HitExplosion(DrawableHitObject judgedObject) { @@ -22,33 +26,32 @@ namespace osu.Game.Rulesets.Mania.UI Anchor = Anchor.TopCentre; Origin = Anchor.Centre; - RelativeSizeAxes = Axes.Both; - Size = new Vector2(isTick ? 0.5f : 1); - FillMode = FillMode.Fit; + RelativeSizeAxes = Axes.X; + Y = NotePiece.NOTE_HEIGHT / 2; + Height = NotePiece.NOTE_HEIGHT; - Blending = BlendingMode.Additive; + // scale roughly in-line with visual appearance of notes + Scale = new Vector2(isTick ? 0.4f : 0.8f); - Color4 accent = isTick ? Color4.White : judgedObject.AccentColour; - - InternalChild = new CircularContainer + InternalChild = circle = new CircularContainer { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, Masking = true, - BorderThickness = 1, - BorderColour = accent, + // we want our size to be very small so the glow dominates it. + Size = new Vector2(0.1f), EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = accent, - Radius = 10, - Hollow = true + Colour = Interpolation.ValueAt(0.1f, judgedObject.AccentColour, Color4.White, 0, 1), + Radius = 100, }, - Child = inner = new Box + Child = new Box { + Alpha = 0, RelativeSizeAxes = Axes.Both, - Colour = accent, - Alpha = 1, - AlwaysPresent = true, + AlwaysPresent = true } }; } @@ -57,8 +60,8 @@ namespace osu.Game.Rulesets.Mania.UI { base.LoadComplete(); - this.ScaleTo(2f, 600, Easing.OutQuint).FadeOut(500); - inner.FadeOut(250); + circle.ResizeTo(circle.Size * new Vector2(4, 20), 1000, Easing.OutQuint); + this.FadeIn(16).Then().FadeOut(500, Easing.OutQuint); Expire(true); } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 7123aab901..a3145d6035 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -10,12 +10,9 @@ using osu.Framework.Input; using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; using osu.Game.Input.Handlers; -using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Mods; -using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Replays; @@ -103,7 +100,5 @@ namespace osu.Game.Rulesets.Mania.UI protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); - - protected override IRulesetConfigManager CreateConfig(Ruleset ruleset, SettingsStore settings) => new ManiaConfigManager(settings, Ruleset.RulesetInfo, Variant); } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index 904be3a9e3..cb93613c7d 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -9,7 +9,6 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -78,6 +77,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, Masking = true, + CornerRadius = 5, Children = new Drawable[] { new Box @@ -171,6 +171,9 @@ namespace osu.Game.Rulesets.Mania.UI internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) { + if (!judgedObject.DisplayJudgement) + return; + judgements.Clear(); judgements.Add(new DrawableManiaJudgement(judgement, judgedObject) { @@ -180,15 +183,15 @@ namespace osu.Game.Rulesets.Mania.UI } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { normalColumnColours = new List { - colours.RedDark, - colours.GreenDark + new Color4(94, 0, 57, 255), + new Color4(6, 84, 0, 255) }; - specialColumnColour = colours.BlueDark; + specialColumnColour = new Color4(0, 48, 63, 255); // Set the special column + colour + key foreach (var column in Columns) diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json index 714fb6db6f..13aba025fd 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json @@ -22,7 +22,7 @@ }, "type": "mono", "request": "launch", - "program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Osu.Tests.exe", + "program": "${workspaceRoot}/bin/Release/net471/osu.Game.Rulesets.Osu.Tests.exe", "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release, msbuild)", "runtimeExecutable": null, @@ -30,12 +30,12 @@ "console": "internalConsole" }, { - "name": "VisualTests (Debug, netcoreapp2.0)", + "name": "VisualTests (Debug, netcoreapp2.1)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Osu.Tests.dll" + "${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Osu.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug, dotnet)", @@ -43,12 +43,12 @@ "console": "internalConsole" }, { - "name": "VisualTests (Release, netcoreapp2.0)", + "name": "VisualTests (Release, netcoreapp2.1)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Osu.Tests.dll" + "${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Osu.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release, dotnet)", diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json index 657fe07e1a..62cf51382f 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json @@ -40,7 +40,7 @@ "build", "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:GenerateFullPaths=true", "/m", "/verbosity:m" @@ -56,7 +56,7 @@ "build", "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:Configuration=Release", "/p:GenerateFullPaths=true", "/m", @@ -75,7 +75,7 @@ "problemMatcher": [] }, { - "label": "Restore (netcoreapp2.0)", + "label": "Restore (netcoreapp2.1)", "type": "shell", "command": "dotnet", "args": [ diff --git a/osu.Game.Rulesets.Osu.Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Osu.Tests/TestCasePerformancePoints.cs deleted file mode 100644 index 63026fe316..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestCasePerformancePoints.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; - -namespace osu.Game.Rulesets.Osu.Tests -{ - [TestFixture] - public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints - { - public TestCasePerformancePoints() - : base(new OsuRuleset()) - { - } - } -} diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 1695ceacee..c5d9b26145 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp2.0;net471 + netcoreapp2.1;net471 diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs new file mode 100644 index 0000000000..50a259ae55 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Osu.Difficulty +{ + public class OsuDifficultyAttributes : DifficultyAttributes + { + public double AimStrain; + public double SpeedStrain; + + public OsuDifficultyAttributes(Mod[] mods, double starRating) + : base(mods, starRating) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 3ed072a275..400afbc043 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -2,12 +2,13 @@ // 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.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Skills; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Difficulty @@ -17,31 +18,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty private const int section_length = 400; private const double difficulty_multiplier = 0.0675; - public OsuDifficultyCalculator(IBeatmap beatmap) - : base(beatmap) + public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { } - public OsuDifficultyCalculator(IBeatmap beatmap, Mod[] mods) - : base(beatmap, mods) + protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) { - } - - public override double Calculate(Dictionary categoryDifficulty = null) - { - OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap((List)Beatmap.HitObjects, TimeRate); + OsuDifficultyBeatmap difficultyBeatmap = new OsuDifficultyBeatmap(beatmap.HitObjects.Cast().ToList(), timeRate); Skill[] skills = { new Aim(), new Speed() }; - double sectionLength = section_length * TimeRate; + double sectionLength = section_length * timeRate; // The first object doesn't generate a strain, so we begin with an incremented section end double currentSectionEnd = 2 * sectionLength; - foreach (OsuDifficultyHitObject h in beatmap) + foreach (OsuDifficultyHitObject h in difficultyBeatmap) { while (h.BaseObject.StartTime > currentSectionEnd) { @@ -60,16 +56,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; - double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2; - if (categoryDifficulty != null) + return new OsuDifficultyAttributes(mods, starRating) { - categoryDifficulty.Add("Aim", aimRating); - categoryDifficulty.Add("Speed", speedRating); - } - - return starRating; + AimStrain = aimRating, + SpeedStrain = speedRating + }; } + + protected override Mod[] DifficultyAdjustmentMods => new Mod[] + { + new OsuModDoubleTime(), + new OsuModHalfTime(), + new OsuModEasy(), + new OsuModHardRock(), + }; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index eeb776fa6e..3ab3cc879a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { + public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes; + private readonly int countHitCircles; private readonly int beatmapMaxCombo; @@ -37,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countMeh; private int countMiss; - public OsuPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) + public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score) : base(ruleset, beatmap, score) { countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle); @@ -61,20 +63,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => !m.Ranked)) return 0; - // Todo: In the future we should apply changes to PreEmpt/AR at an OsuHitObject/BaseDifficulty level, but this is done - // locally for now as doing so would modify animations and other things unexpectedly - // DO NOT MODIFY THIS - double ar = Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate; - if (mods.Any(m => m is OsuModHardRock)) - ar = Math.Min(10, ar * 1.4); - if (mods.Any(m => m is OsuModEasy)) - ar = Math.Max(0, ar / 2); - - double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / TimeRate; - double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future + double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate; + double preEmpt = (int)BeatmapDifficulty.DifficultyRange(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / TimeRate; realApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5; - realOverallDifficulty = (80 - 0.5 - hitWindowGreat) / 6; + realOverallDifficulty = (80 - hitWindowGreat) / 6; // Custom multipliers for NoFail and SpunOut. double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things @@ -110,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue() { - double aimValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Aim"] / 0.0675f) - 4.0f, 3.0f) / 100000.0f; + double aimValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes.AimStrain / 0.0675f) - 4.0f, 3.0f) / 100000.0f; // Longer maps are worth more double lengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + @@ -159,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeSpeedValue() { - double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Speed"] / 0.0675f) - 4.0f, 3.0f) / 100000.0f; + double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes.SpeedStrain / 0.0675f) - 4.0f, 3.0f) / 100000.0f; // Longer maps are worth more speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 3573c133c2..b2ddd65e38 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -13,8 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string ShortenedName => "AP"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_autopilot; public override string Description => @"Automatic cursor movement - just follow the rhythm."; - public override double ScoreMultiplier => 0; - public override bool Ranked => false; + public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) }; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index e72d667c0b..4220b72b16 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -7,33 +7,34 @@ using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModHidden : ModHidden, IApplicableToDrawableHitObjects + public class OsuModHidden : ModHidden { public override string Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => 1.06; - private const double fade_in_duration_multiplier = 0.4; private const double fade_out_duration_multiplier = 0.3; - public void ApplyToDrawableHitObjects(IEnumerable drawables) + public override void ApplyToDrawableHitObjects(IEnumerable drawables) { + void adjustFadeIn(OsuHitObject h) => h.TimeFadein = h.TimePreempt * fade_in_duration_multiplier; + foreach (var d in drawables.OfType()) { - d.ApplyCustomUpdateState += ApplyHiddenState; - - d.HitObject.TimeFadein = d.HitObject.TimePreempt * fade_in_duration_multiplier; + adjustFadeIn(d.HitObject); foreach (var h in d.HitObject.NestedHitObjects.OfType()) - h.TimeFadein = h.TimePreempt * fade_in_duration_multiplier; + adjustFadeIn(h); } + + base.ApplyToDrawableHitObjects(drawables); } - protected void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) + protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) { if (!(drawable is DrawableOsuHitObject d)) return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index b3bc2930d8..26f3ee6bb4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; +using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Drawables; using OpenTK; using osu.Game.Graphics; @@ -89,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // find the next vector2 in the curve which is not equal to our current position to infer a rotation. for (int i = searchStart; i >= 0 && i < curve.Count; i += direction) { - if (curve[i] == Position) + if (Precision.AlmostEquals(curve[i], Position)) continue; Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(curve[i].Y - Position.Y, curve[i].X - Position.X)); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 0a6b1b459a..94a61e7904 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -139,8 +139,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var texture = new Texture(textureWidth, 1); //initialise background - var upload = new TextureUpload(textureWidth * 4); - var bytes = upload.Data; + var raw = new RawTexture(textureWidth, 1); + var bytes = raw.Data; const float aa_portion = 0.02f; const float border_portion = 0.128f; @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - texture.SetData(upload); + texture.SetData(new TextureUpload(raw)); path.Texture = texture; container.ForceRedraw(); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index c455bb2af6..ce80537b93 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -93,57 +93,26 @@ namespace osu.Game.Rulesets.Osu { new OsuModEasy(), new OsuModNoFail(), - new MultiMod - { - Mods = new Mod[] - { - new OsuModHalfTime(), - new OsuModDaycore(), - }, - }, + new MultiMod(new OsuModHalfTime(), new OsuModDaycore()), }; - case ModType.DifficultyIncrease: return new Mod[] { new OsuModHardRock(), - new MultiMod - { - Mods = new Mod[] - { - new OsuModSuddenDeath(), - new OsuModPerfect(), - }, - }, - new MultiMod - { - Mods = new Mod[] - { - new OsuModDoubleTime(), - new OsuModNightcore(), - }, - }, + new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()), + new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new OsuModHidden(), new OsuModFlashlight(), }; - case ModType.Special: return new Mod[] { new OsuModRelax(), new OsuModAutopilot(), new OsuModSpunOut(), - new MultiMod - { - Mods = new Mod[] - { - new OsuModAutoplay(), - new ModCinema(), - }, - }, + new MultiMod(new OsuModAutoplay(), new ModCinema()), new OsuModTarget(), }; - default: return new Mod[] { }; } @@ -151,9 +120,9 @@ namespace osu.Game.Rulesets.Osu public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o }; - public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods); + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score); public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); @@ -161,7 +130,7 @@ namespace osu.Game.Rulesets.Osu public override string ShortName => "osu"; - public override SettingsSubsection CreateSettings() => new OsuSettings(); + public override RulesetSettingsSubsection CreateSettings() => new OsuSettings(this); public override int? LegacyID => 0; diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs index e7f17dd86b..35146dfe29 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private Bindable cursorScale; private Bindable autoCursorScale; - private Bindable beatmap; + private readonly IBindable beatmap = new Bindable(); public OsuCursor() { @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, OsuGameBase game) + private void load(OsuConfigManager config, IBindableBeatmap beatmap) { Child = cursorContainer = new SkinnableDrawable("cursor", _ => new CircularContainer { @@ -160,8 +160,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor RelativeSizeAxes = Axes.Both, }; - beatmap = game.Beatmap.GetBoundCopy(); - beatmap.ValueChanged += v => calculateScale(); + this.beatmap.BindTo(beatmap); + this.beatmap.ValueChanged += v => calculateScale(); cursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); cursorScale.ValueChanged += v => calculateScale(); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index b63f85623e..f2d5631e93 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -64,9 +64,7 @@ namespace osu.Game.Rulesets.Osu.UI public override void PostProcess() { - connectionLayer.HitObjects = HitObjects.Objects - .Select(d => d.HitObject) - .OrderBy(h => h.StartTime).OfType(); + connectionLayer.HitObjects = HitObjects.Objects.Select(d => d.HitObject).OfType(); } private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index ad1052f86a..33d4e16662 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -18,7 +18,7 @@ using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Osu.UI { - public class OsuRulesetContainer : RulesetContainer + public class OsuRulesetContainer : RulesetContainer { public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettings.cs b/osu.Game.Rulesets.Osu/UI/OsuSettings.cs index 31ad6701fd..25c009b117 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettings.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettings.cs @@ -8,10 +8,15 @@ using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Osu.UI { - public class OsuSettings : SettingsSubsection + public class OsuSettings : RulesetSettingsSubsection { protected override string Header => "osu!"; + public OsuSettings(Ruleset ruleset) + : base(ruleset) + { + } + [BackgroundDependencyLoader] private void load(OsuConfigManager config) { diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json index e1df54e99b..df49e177dc 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json @@ -22,7 +22,7 @@ }, "type": "mono", "request": "launch", - "program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Taiko.Tests.exe", + "program": "${workspaceRoot}/bin/Release/net471/osu.Game.Rulesets.Taiko.Tests.exe", "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release, msbuild)", "runtimeExecutable": null, @@ -30,12 +30,12 @@ "console": "internalConsole" }, { - "name": "VisualTests (Debug, netcoreapp2.0)", + "name": "VisualTests (Debug, netcoreapp2.1)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Taiko.Tests.dll" + "${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Taiko.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Debug, dotnet)", @@ -43,12 +43,12 @@ "console": "internalConsole" }, { - "name": "VisualTests (Release, netcoreapp2.0)", + "name": "VisualTests (Release, netcoreapp2.1)", "type": "coreclr", "request": "launch", "program": "dotnet", "args": [ - "${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Taiko.Tests.dll" + "${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Taiko.Tests.dll" ], "cwd": "${workspaceRoot}", "preLaunchTask": "Build (Release, dotnet)", diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json index 8bdbcd8e8e..7c8beed00f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json @@ -40,7 +40,7 @@ "build", "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:GenerateFullPaths=true", "/m", "/verbosity:m" @@ -56,7 +56,7 @@ "build", "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", - "/p:TargetFramework=netcoreapp2.0", + "/p:TargetFramework=netcoreapp2.1", "/p:Configuration=Release", "/p:GenerateFullPaths=true", "/m", @@ -75,7 +75,7 @@ "problemMatcher": [] }, { - "label": "Restore (netcoreapp2.0)", + "label": "Restore (netcoreapp2.1)", "type": "shell", "command": "dotnet", "args": [ diff --git a/osu.Game.Rulesets.Taiko.Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Taiko.Tests/TestCasePerformancePoints.cs deleted file mode 100644 index 2fd9161d13..0000000000 --- a/osu.Game.Rulesets.Taiko.Tests/TestCasePerformancePoints.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using NUnit.Framework; - -namespace osu.Game.Rulesets.Taiko.Tests -{ - [TestFixture] - public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints - { - public TestCasePerformancePoints() - : base(new TaikoRuleset()) - { - } - } -} diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index 1221584a2b..dea34d25e7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp2.0;net471 + netcoreapp2.1;net471 diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 57e1e65064..473c205293 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty @@ -26,46 +27,33 @@ namespace osu.Game.Rulesets.Taiko.Difficulty /// private const double decay_weight = 0.9; - /// - /// HitObjects are stored as a member variable. - /// - private readonly List difficultyHitObjects = new List(); - - public TaikoDifficultyCalculator(IBeatmap beatmap) - : base(beatmap) + public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) { } - public TaikoDifficultyCalculator(IBeatmap beatmap, Mod[] mods) - : base(beatmap, mods) + protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) { - } + var difficultyHitObjects = new List(); - public override double Calculate(Dictionary categoryDifficulty = null) - { - // Fill our custom DifficultyHitObject class, that carries additional information - difficultyHitObjects.Clear(); - - foreach (var hitObject in Beatmap.HitObjects) + foreach (var hitObject in beatmap.HitObjects) difficultyHitObjects.Add(new TaikoHitObjectDifficulty((TaikoHitObject)hitObject)); // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); - if (!calculateStrainValues()) return 0; + if (!calculateStrainValues(difficultyHitObjects, timeRate)) + return new DifficultyAttributes(mods, 0); - double starRating = calculateDifficulty() * star_scaling_factor; + double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor; - if (categoryDifficulty != null) - categoryDifficulty["Strain"] = starRating; - - return starRating; + return new DifficultyAttributes(mods, starRating); } - private bool calculateStrainValues() + private bool calculateStrainValues(List objects, double timeRate) { // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. - using (List.Enumerator hitObjectsEnumerator = difficultyHitObjects.GetEnumerator()) + using (var hitObjectsEnumerator = objects.GetEnumerator()) { if (!hitObjectsEnumerator.MoveNext()) return false; @@ -75,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty while (hitObjectsEnumerator.MoveNext()) { var next = hitObjectsEnumerator.Current; - next?.CalculateStrains(current, TimeRate); + next?.CalculateStrains(current, timeRate); current = next; } @@ -83,9 +71,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } } - private double calculateDifficulty() + private double calculateDifficulty(List objects, double timeRate) { - double actualStrainStep = strain_step * TimeRate; + double actualStrainStep = strain_step * timeRate; // Find the highest strain value within each strain step List highestStrains = new List(); @@ -93,7 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval TaikoHitObjectDifficulty previousHitObject = null; - foreach (var hitObject in difficultyHitObjects) + foreach (var hitObject in objects) { // While we are beyond the current interval push the currently available maximum to our strain list while (hitObject.BaseHitObject.StartTime > intervalEndTime) @@ -135,5 +123,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return difficulty; } + + protected override Mod[] DifficultyAdjustmentMods => new Mod[] + { + new TaikoModDoubleTime(), + new TaikoModHalfTime(), + new TaikoModEasy(), + new TaikoModHardRock(), + }; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9c9cd1f0fb..53cfb4fd0f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -22,10 +22,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countMeh; private int countMiss; - public TaikoPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) + public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score) : base(ruleset, beatmap, score) { - beatmapMaxCombo = beatmap.HitObjects.Count(h => h is Hit); + beatmapMaxCombo = Beatmap.HitObjects.Count(h => h is Hit); } public override double Calculate(Dictionary categoryDifficulty = null) @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeStrainValue() { - double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes["Strain"] / 0.0075) - 4.0, 2.0) / 100000.0; + double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; // Longer maps are worth more double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0); @@ -94,7 +94,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double computeAccuracyValue() { - double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future + double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate; if (hitWindowGreat <= 0) return 0; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index d923b2dcdf..519b56a3ed 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -86,6 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (State.Value) { case ArmedState.Idle: + UnproxyContent(); this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire(); break; case ArmedState.Miss: @@ -93,6 +94,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables .Expire(); break; case ArmedState.Hit: + // If we're far enough away from the left stage, we should bring outselves in front of it + if (X >= -0.05f) + ProxyContent(); + var flash = circlePiece?.FlashBox; if (flash != null) { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 33cc29bccf..df36a475d6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -20,12 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableSwell : DrawableTaikoHitObject { - /// - /// Invoked when the swell has reached the hit target, i.e. when CurrentTime >= StartTime. - /// This is only ever invoked once. - /// - public event Action OnStart; - private const float target_ring_thick_border = 1.4f; private const float target_ring_thin_border = 1f; private const float target_ring_scale = 5f; @@ -40,7 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private int userHits; - private bool hasStarted; private readonly SwellSymbolPiece symbol; public DrawableSwell(Swell swell) @@ -48,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - AddInternal(bodyContainer = new Container + Content.Add(bodyContainer = new Container { RelativeSizeAxes = Axes.Both, Depth = 1, @@ -177,6 +170,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (state) { + case ArmedState.Idle: + UnproxyContent(); + break; case ArmedState.Hit: bodyContainer.Delay(untilJudgement).ScaleTo(1.4f, out_transition_time); break; @@ -195,11 +191,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables X = Math.Max(0, X); double t = Math.Min(HitObject.StartTime, Time.Current); - if (t == HitObject.StartTime && !hasStarted) - { - OnStart?.Invoke(); - hasStarted = true; - } + if (t == HitObject.StartTime) + ProxyContent(); } private bool? lastWasCentre; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 971fd8854d..a6d61f1a5a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -9,10 +9,75 @@ using OpenTK; using System.Linq; using osu.Game.Audio; using System.Collections.Generic; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public abstract class DrawableTaikoHitObject : DrawableHitObject, IKeyBindingHandler + public abstract class DrawableTaikoHitObject : DrawableHitObject, IKeyBindingHandler + { + protected readonly Container Content; + private readonly Container proxiedContent; + + private readonly Container nonProxiedContent; + + protected DrawableTaikoHitObject(TaikoHitObject hitObject) + : base(hitObject) + { + InternalChildren = new[] + { + nonProxiedContent = new Container + { + RelativeSizeAxes = Axes.Both, + Child = Content = new Container { RelativeSizeAxes = Axes.Both } + }, + proxiedContent = new Container { RelativeSizeAxes = Axes.Both } + }; + } + + /// + /// is proxied into an upper layer. We don't want to get masked away otherwise would too. + /// + protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; + + private bool isProxied; + + /// + /// Moves to a layer proxied above the playfield. + /// Does nothing is content is already proxied. + /// + protected void ProxyContent() + { + if (isProxied) return; + isProxied = true; + + nonProxiedContent.Remove(Content); + proxiedContent.Add(Content); + } + + /// + /// Moves to the normal hitobject layer. + /// Does nothing is content is not currently proxied. + /// + protected void UnproxyContent() + { + if (!isProxied) return; + isProxied = false; + + proxiedContent.Remove(Content); + nonProxiedContent.Add(Content); + } + + /// + /// Creates a proxy for the content of this . + /// + public Drawable CreateProxiedContent() => proxiedContent.CreateProxy(); + + public abstract bool OnPressed(TaikoAction action); + public virtual bool OnReleased(TaikoAction action) => false; + } + + public abstract class DrawableTaikoHitObject : DrawableTaikoHitObject where TaikoHitType : TaikoHitObject { public override Vector2 OriginPosition => new Vector2(DrawHeight / 2); @@ -34,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables RelativeSizeAxes = Axes.Both; Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); - InternalChild = MainPiece = CreateMainPiece(); + Content.Add(MainPiece = CreateMainPiece()); MainPiece.KiaiMode = HitObject.Kiai; } @@ -44,9 +109,5 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override string SampleNamespace => "Taiko"; protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(); - - public abstract bool OnPressed(TaikoAction action); - - public virtual bool OnReleased(TaikoAction action) => false; } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index abaa8db597..7b4978694b 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -84,56 +84,25 @@ namespace osu.Game.Rulesets.Taiko { new TaikoModEasy(), new TaikoModNoFail(), - new MultiMod - { - Mods = new Mod[] - { - new TaikoModHalfTime(), - new TaikoModDaycore(), - }, - }, + new MultiMod(new TaikoModHalfTime(), new TaikoModDaycore()), }; - case ModType.DifficultyIncrease: return new Mod[] { new TaikoModHardRock(), - new MultiMod - { - Mods = new Mod[] - { - new TaikoModSuddenDeath(), - new TaikoModPerfect(), - }, - }, - new MultiMod - { - Mods = new Mod[] - { - new TaikoModDoubleTime(), - new TaikoModNightcore(), - }, - }, + new MultiMod(new TaikoModSuddenDeath(), new TaikoModPerfect()), + new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()), new TaikoModHidden(), new TaikoModFlashlight(), }; - case ModType.Special: return new Mod[] { new TaikoModRelax(), null, null, - new MultiMod - { - Mods = new Mod[] - { - new TaikoModAutoplay(), - new ModCinema(), - }, - }, + new MultiMod(new TaikoModAutoplay(), new ModCinema()), }; - default: return new Mod[] { }; } @@ -145,9 +114,9 @@ namespace osu.Game.Rulesets.Taiko public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o }; - public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap, mods); + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new TaikoPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => new TaikoPerformanceCalculator(this, beatmap, score); public override int? LegacyID => 1; diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index ee2c1d5ad5..4dd0ba4d3d 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Taiko.UI /// internal class HitExplosion : CircularContainer { + public override bool RemoveWhenNotAlive => true; + public readonly DrawableHitObject JudgedObject; private readonly Box innerFill; @@ -66,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.UI this.ScaleTo(3f, 1000, Easing.OutQuint); this.FadeOut(500); - Expire(); + Expire(true); } /// diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index ac3cf8305a..287d59972a 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -14,6 +14,8 @@ namespace osu.Game.Rulesets.Taiko.UI { public class KiaiHitExplosion : CircularContainer { + public override bool RemoveWhenNotAlive => true; + public readonly DrawableHitObject JudgedObject; private readonly bool isRim; @@ -62,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.UI this.ScaleTo(new Vector2(1, 3f), 500, Easing.OutQuint); this.FadeOut(250); - Expire(); + Expire(true); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 417a7c2581..7fdd3cd1e2 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -216,10 +216,9 @@ namespace osu.Game.Rulesets.Taiko.UI if (barline != null) barlineContainer.Add(barline.CreateProxy()); - // Swells should be moved at the very top of the playfield when they reach the hit target - var swell = h as DrawableSwell; - if (swell != null) - swell.OnStart += () => topLevelHitContainer.Add(swell.CreateProxy()); + var taikoObject = h as DrawableTaikoHitObject; + if (taikoObject != null) + topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); } internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) @@ -244,19 +243,6 @@ namespace osu.Game.Rulesets.Taiko.UI hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == judgedObject)?.VisualiseSecondHit(); else { - if (judgedObject.X >= -0.05f && judgedObject is DrawableHit) - { - // If we're far enough away from the left stage, we should bring outselves in front of it - // Todo: The following try-catch is temporary for replay rewinding support - try - { - topLevelHitContainer.Add(judgedObject.CreateProxy()); - } - catch - { - } - } - hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim)); if (judgedObject.HitObject.Kiai) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 4985aa9365..1628423fe8 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(string.Empty, metadata.Source); Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags); Assert.AreEqual(557821, beatmapInfo.OnlineBeatmapID); - Assert.AreEqual(241526, metadata.OnlineBeatmapSetID); + Assert.AreEqual(241526, beatmapInfo.BeatmapSet.OnlineBeatmapSetID); } } diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 489c38c420..b834be71f1 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = decodeAsJson(normal); var meta = beatmap.BeatmapInfo.Metadata; - Assert.AreEqual(241526, meta.OnlineBeatmapSetID); + Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID); Assert.AreEqual("Soleily", meta.Artist); Assert.AreEqual("Soleily", meta.ArtistUnicode); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index f60caf2397..616ba132fd 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -12,13 +12,14 @@ using osu.Framework.Platform; using osu.Game.IPC; using osu.Framework.Allocation; using osu.Game.Beatmaps; +using SharpCompress.Archives.Zip; namespace osu.Game.Tests.Beatmaps.IO { [TestFixture] public class ImportBeatmapTest { - private const string osz_path = @"../../../../osu-resources/osu.Game.Resources/Beatmaps/241526 Soleily - Renatus.osz"; + public const string TEST_OSZ_PATH = @"../../../../osu-resources/osu.Game.Resources/Beatmaps/241526 Soleily - Renatus.osz"; [Test] public void TestImportWhenClosed() @@ -77,8 +78,69 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); + Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestRollbackOnFailure() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestRollbackOnFailure")) + { + try + { + var osu = loadOsu(host); + var manager = osu.Dependencies.Get(); + + int fireCount = 0; + + // ReSharper disable once AccessToModifiedClosure + manager.ItemAdded += _ => fireCount++; + manager.ItemRemoved += _ => fireCount++; + + var imported = loadOszIntoOsu(osu); + + Assert.AreEqual(0, fireCount -= 1); + + imported.Hash += "-changed"; + manager.Update(imported); + + Assert.AreEqual(0, fireCount -= 2); + + var breakTemp = createTemporaryBeatmap(); + + MemoryStream brokenOsu = new MemoryStream(new byte[] { 1, 3, 3, 7 }); + MemoryStream brokenOsz = new MemoryStream(File.ReadAllBytes(breakTemp)); + + File.Delete(breakTemp); + + using (var outStream = File.Open(breakTemp, FileMode.CreateNew)) + using (var zip = ZipArchive.Open(brokenOsz)) + { + zip.AddEntry("broken.osu", brokenOsu, false); + zip.SaveTo(outStream, SharpCompress.Common.CompressionType.Deflate); + } + + Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); + Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); + Assert.AreEqual(12, manager.QueryBeatmaps(_ => true).ToList().Count); + + // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu. + manager.Import(breakTemp); + + // no events should be fired in the case of a rollback. + Assert.AreEqual(0, fireCount); + + Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); + Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); + Assert.AreEqual(12, manager.QueryBeatmaps(_ => true).ToList().Count); } finally { @@ -100,18 +162,17 @@ namespace osu.Game.Tests.Beatmaps.IO var imported = loadOszIntoOsu(osu); - //var change = manager.QueryBeatmapSets(_ => true).First(); imported.Hash += "-changed"; manager.Update(imported); var importedSecondTime = loadOszIntoOsu(osu); - // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. Assert.IsTrue(imported.ID != importedSecondTime.ID); Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID); - Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + // only one beatmap will exist as the online set ID matched, causing purging of the first import. + Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); + Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); } finally { @@ -162,8 +223,7 @@ namespace osu.Game.Tests.Beatmaps.IO var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp)); + var temp = createTemporaryBeatmap(); var importer = new ArchiveImportIPCChannel(client); if (!importer.ImportAsync(temp).Wait(10000)) @@ -188,8 +248,7 @@ namespace osu.Game.Tests.Beatmaps.IO try { var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); - Assert.IsTrue(File.Exists(temp), "Temporary file copy never substantiated"); + var temp = createTemporaryBeatmap(); using (File.OpenRead(temp)) osu.Dependencies.Get().Import(temp); ensureLoaded(osu); @@ -203,11 +262,17 @@ namespace osu.Game.Tests.Beatmaps.IO } } - private BeatmapSetInfo loadOszIntoOsu(OsuGameBase osu) + private string createTemporaryBeatmap() { - var temp = prepareTempCopy(osz_path); - + var temp = Path.GetTempFileName() + ".osz"; + File.Copy(TEST_OSZ_PATH, temp, true); Assert.IsTrue(File.Exists(temp)); + return temp; + } + + private BeatmapSetInfo loadOszIntoOsu(OsuGameBase osu, string path = null) + { + var temp = path ?? createTemporaryBeatmap(); var manager = osu.Dependencies.Get(); @@ -219,7 +284,7 @@ namespace osu.Game.Tests.Beatmaps.IO waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return imported.FirstOrDefault(); + return imported.LastOrDefault(); } private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) @@ -228,16 +293,10 @@ namespace osu.Game.Tests.Beatmaps.IO manager.Delete(imported); Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); - Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } - private string prepareTempCopy(string path) - { - var temp = Path.GetTempFileName(); - return new FileInfo(path).CopyTo(temp, true).FullName; - } - private OsuGameBase loadOsu(GameHost host) { var osu = new OsuGameBase(); @@ -286,12 +345,12 @@ namespace osu.Game.Tests.Beatmaps.IO private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) { - Action waitAction = () => + Task task = Task.Run(() => { while (!result()) Thread.Sleep(200); - }; + }); - Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(timeout), failureMessage); + Assert.IsTrue(task.Wait(timeout), failureMessage); } } } diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index c6863d1cb5..0039516c0c 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -48,17 +48,20 @@ namespace osu.Game.Tests.Beatmaps.IO { var reader = new ZipArchiveReader(osz); - BeatmapMetadata meta; - using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu"))) - meta = Decoder.GetDecoder(stream).Decode(stream).Metadata; + Beatmap beatmap; - Assert.AreEqual(241526, meta.OnlineBeatmapSetID); + using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu"))) + beatmap = Decoder.GetDecoder(stream).Decode(stream); + + var meta = beatmap.Metadata; + + Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID); Assert.AreEqual("Soleily", meta.Artist); Assert.AreEqual("Soleily", meta.ArtistUnicode); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); Assert.AreEqual("Deif", meta.AuthorString); Assert.AreEqual("machinetop_background.jpg", meta.BackgroundFile); - Assert.AreEqual(164471 + LegacyBeatmapDecoder.UniversalOffset, meta.PreviewTime); + Assert.AreEqual(164471, meta.PreviewTime); Assert.AreEqual(string.Empty, meta.Source); Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", meta.Tags); Assert.AreEqual("Renatus", meta.Title); diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs new file mode 100644 index 0000000000..49494b65b9 --- /dev/null +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -0,0 +1,152 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class DifficultyAdjustmentModCombinationsTest + { + [Test] + public void TestNoMods() + { + var combinations = new TestDifficultyCalculator().CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(1, combinations.Length); + Assert.IsTrue(combinations[0] is NoModMod); + } + + [Test] + public void TestSingleMod() + { + var combinations = new TestDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(2, combinations.Length); + Assert.IsTrue(combinations[0] is NoModMod); + Assert.IsTrue(combinations[1] is ModA); + } + + [Test] + public void TestDoubleMod() + { + var combinations = new TestDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(4, combinations.Length); + Assert.IsTrue(combinations[0] is NoModMod); + Assert.IsTrue(combinations[1] is ModA); + Assert.IsTrue(combinations[2] is MultiMod); + Assert.IsTrue(combinations[3] is ModB); + + Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); + } + + [Test] + public void TestIncompatibleMods() + { + var combinations = new TestDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(3, combinations.Length); + Assert.IsTrue(combinations[0] is NoModMod); + Assert.IsTrue(combinations[1] is ModA); + Assert.IsTrue(combinations[2] is ModIncompatibleWithA); + } + + [Test] + public void TestDoubleIncompatibleMods() + { + var combinations = new TestDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(8, combinations.Length); + Assert.IsTrue(combinations[0] is NoModMod); + Assert.IsTrue(combinations[1] is ModA); + Assert.IsTrue(combinations[2] is MultiMod); + Assert.IsTrue(combinations[3] is ModB); + Assert.IsTrue(combinations[4] is MultiMod); + Assert.IsTrue(combinations[5] is ModIncompatibleWithA); + Assert.IsTrue(combinations[6] is MultiMod); + Assert.IsTrue(combinations[7] is ModIncompatibleWithAAndB); + + Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); + Assert.IsTrue(((MultiMod)combinations[4]).Mods[0] is ModB); + Assert.IsTrue(((MultiMod)combinations[4]).Mods[1] is ModIncompatibleWithA); + Assert.IsTrue(((MultiMod)combinations[6]).Mods[0] is ModIncompatibleWithA); + Assert.IsTrue(((MultiMod)combinations[6]).Mods[1] is ModIncompatibleWithAAndB); + } + + [Test] + public void TestIncompatibleThroughBaseType() + { + var combinations = new TestDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(3, combinations.Length); + Assert.IsTrue(combinations[0] is NoModMod); + Assert.IsTrue(combinations[1] is ModAofA); + Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA); + } + + private class ModA : Mod + { + public override string Name => nameof(ModA); + public override string ShortenedName => nameof(ModA); + public override double ScoreMultiplier => 1; + + public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) }; + } + + private class ModB : Mod + { + public override string Name => nameof(ModB); + public override string ShortenedName => nameof(ModB); + public override double ScoreMultiplier => 1; + + public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) }; + } + + private class ModIncompatibleWithA : Mod + { + public override string Name => $"Incompatible With {nameof(ModA)}"; + public override string ShortenedName => $"Incompatible With {nameof(ModA)}"; + public override double ScoreMultiplier => 1; + + public override Type[] IncompatibleMods => new[] { typeof(ModA) }; + } + + private class ModAofA : ModA + { + } + + private class ModIncompatibleWithAofA : ModIncompatibleWithA + { + // Incompatible through base type + } + + private class ModIncompatibleWithAAndB : Mod + { + public override string Name => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}"; + public override string ShortenedName => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}"; + public override double ScoreMultiplier => 1; + + public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) }; + } + + private class TestDifficultyCalculator : DifficultyCalculator + { + public TestDifficultyCalculator(params Mod[] mods) + : base(null, null) + { + DifficultyAdjustmentMods = mods; + } + + protected override Mod[] DifficultyAdjustmentMods { get; } + + protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) => throw new NotImplementedException(); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseAutoplay.cs b/osu.Game.Tests/Visual/TestCaseAutoplay.cs index cecb327b6c..4abfec4371 100644 --- a/osu.Game.Tests/Visual/TestCaseAutoplay.cs +++ b/osu.Game.Tests/Visual/TestCaseAutoplay.cs @@ -3,8 +3,8 @@ using System.ComponentModel; using System.Linq; -using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual @@ -12,10 +12,22 @@ namespace osu.Game.Tests.Visual [Description("Player instantiated with an autoplay mod.")] public class TestCaseAutoplay : TestCasePlayer { - protected override Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) + protected override Player CreatePlayer(Ruleset ruleset) { - beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); - return base.CreatePlayer(beatmap, ruleset); + Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); + return new ScoreAccessiblePlayer + { + AllowPause = false, + AllowLeadIn = false, + AllowResults = false, + }; + } + + protected override bool ContinueCondition(Player player) => base.ContinueCondition(player) && ((ScoreAccessiblePlayer)player).ScoreProcessor.TotalScore > 0; + + private class ScoreAccessiblePlayer : Player + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; } } } diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs index f819e882d9..6d2b37d981 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs @@ -65,11 +65,7 @@ namespace osu.Game.Tests.Visual carousel.SelectionChanged = s => currentSelection = s; - AddStep("Load Beatmaps", () => { carousel.BeatmapSets = beatmapSets; }); - - bool changed = false; - carousel.BeatmapSetsChanged = () => changed = true; - AddUntilStep(() => changed, "Wait for load"); + loadBeatmaps(beatmapSets); testTraversal(); testFiltering(); @@ -84,6 +80,17 @@ namespace osu.Game.Tests.Visual testCarouselRootIsRandom(); } + private void loadBeatmaps(List beatmapSets) + { + bool changed = false; + AddStep($"Load {beatmapSets.Count} Beatmaps", () => + { + carousel.BeatmapSetsChanged = () => changed = true; + carousel.BeatmapSets = beatmapSets; + }); + AddUntilStep(() => changed, "Wait for load"); + } + private void ensureRandomFetchSuccess() => AddAssert("ensure prev random fetch worked", () => selectedSets.Peek() == carousel.SelectedBeatmapSet); @@ -423,7 +430,7 @@ namespace osu.Game.Tests.Visual for (int i = 1; i <= 50; i++) beatmapSets.Add(createTestBeatmapSet(i)); - AddStep("Load 50 Beatmaps", () => { carousel.BeatmapSets = beatmapSets; }); + loadBeatmaps(beatmapSets); advanceSelection(direction: 1, diff: false); checkNonmatchingFilter(); checkNonmatchingFilter(); @@ -442,7 +449,6 @@ namespace osu.Game.Tests.Visual Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), Metadata = new BeatmapMetadata { - OnlineBeatmapSetID = id, // Create random metadata, then we can check if sorting works based on these Artist = $"peppy{id.ToString().PadLeft(6, '0')}", Title = $"test set #{id}!", @@ -496,7 +502,6 @@ namespace osu.Game.Tests.Visual Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), Metadata = new BeatmapMetadata { - OnlineBeatmapSetID = id, // Create random metadata, then we can check if sorting works based on these Artist = $"peppy{id.ToString().PadLeft(6, '0')}", Title = $"test set #{id}!", diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs index 0d3e08154f..f52fecfd02 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using OpenTK; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -29,14 +28,11 @@ namespace osu.Game.Tests.Visual private RulesetStore rulesets; private TestBeatmapInfoWedge infoWedge; private readonly List beatmaps = new List(); - private readonly Bindable beatmap = new Bindable(); [BackgroundDependencyLoader] - private void load(OsuGameBase game, RulesetStore rulesets) + private void load(RulesetStore rulesets) { this.rulesets = rulesets; - - beatmap.BindTo(game.Beatmap); } protected override void LoadComplete() @@ -53,11 +49,11 @@ namespace osu.Game.Tests.Visual AddStep("show", () => { infoWedge.State = Visibility.Visible; - infoWedge.UpdateBeatmap(beatmap); + infoWedge.Beatmap = Beatmap; }); // select part is redundant, but wait for load isn't - selectBeatmap(beatmap.Value.Beatmap); + selectBeatmap(Beatmap.Value.Beatmap); AddWaitStep(3); @@ -120,8 +116,8 @@ namespace osu.Game.Tests.Visual { selectNullBeatmap(); AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text)); - AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == beatmap.Default.BeatmapInfo.Metadata.Title); - AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == beatmap.Default.BeatmapInfo.Metadata.Artist); + AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title); + AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist); AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any()); AddAssert("check no infolabels", () => !infoWedge.Info.InfoLabelContainer.Children.Any()); } @@ -133,7 +129,7 @@ namespace osu.Game.Tests.Visual AddStep($"select {b.Metadata.Title} beatmap", () => { infoBefore = infoWedge.Info; - infoWedge.UpdateBeatmap(beatmap.Value = new TestWorkingBeatmap(b)); + infoWedge.Beatmap = Beatmap.Value = new TestWorkingBeatmap(b); }); AddUntilStep(() => infoWedge.Info != infoBefore, "wait for async load"); @@ -143,8 +139,8 @@ namespace osu.Game.Tests.Visual { AddStep("select null beatmap", () => { - beatmap.Value = beatmap.Default; - infoWedge.UpdateBeatmap(beatmap); + Beatmap.Value = Beatmap.Default; + infoWedge.Beatmap = Beatmap; }); } diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapScoresContainer.cs b/osu.Game.Tests/Visual/TestCaseBeatmapScoresContainer.cs index 5be7386238..d3098864f4 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapScoresContainer.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapScoresContainer.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.MathUtils; using osu.Game.Graphics; -using osu.Game.Online.API.Requests; using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -15,6 +14,7 @@ using osu.Game.Users; using System.Collections.Generic; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets.Osu; namespace osu.Game.Tests.Visual @@ -22,9 +22,9 @@ namespace osu.Game.Tests.Visual [System.ComponentModel.Description("in BeatmapOverlay")] public class TestCaseBeatmapScoresContainer : OsuTestCase { - private readonly IEnumerable scores; - private readonly IEnumerable anotherScores; - private readonly OnlineScore topScore; + private readonly IEnumerable scores; + private readonly IEnumerable anotherScores; + private readonly APIScore topScore; private readonly Box background; public TestCaseBeatmapScoresContainer() @@ -52,12 +52,12 @@ namespace osu.Game.Tests.Visual AddStep("remove scores", () => scoresContainer.Scores = null); AddStep("resize to big", () => container.ResizeWidthTo(1, 300)); AddStep("resize to normal", () => container.ResizeWidthTo(0.8f, 300)); - AddStep("online scores", () => scoresContainer.Beatmap = new BeatmapInfo { OnlineBeatmapSetID = 1, OnlineBeatmapID = 75, Ruleset = new OsuRuleset().RulesetInfo }); + AddStep("online scores", () => scoresContainer.Beatmap = new BeatmapInfo { OnlineBeatmapID = 75, Ruleset = new OsuRuleset().RulesetInfo }); scores = new[] { - new OnlineScore + new APIScore { User = new User { @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual TotalScore = 1234567890, Accuracy = 1, }, - new OnlineScore + new APIScore { User = new User { @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual TotalScore = 1234789, Accuracy = 0.9997, }, - new OnlineScore + new APIScore { User = new User { @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual TotalScore = 12345678, Accuracy = 0.9854, }, - new OnlineScore + new APIScore { User = new User { @@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual TotalScore = 1234567, Accuracy = 0.8765, }, - new OnlineScore + new APIScore { User = new User { @@ -169,7 +169,7 @@ namespace osu.Game.Tests.Visual anotherScores = new[] { - new OnlineScore + new APIScore { User = new User { @@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual TotalScore = 1234789, Accuracy = 0.9997, }, - new OnlineScore + new APIScore { User = new User { @@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual TotalScore = 1234567890, Accuracy = 1, }, - new OnlineScore + new APIScore { User = new User { @@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual TotalScore = 123456, Accuracy = 0.6543, }, - new OnlineScore + new APIScore { User = new User { @@ -251,7 +251,7 @@ namespace osu.Game.Tests.Visual TotalScore = 12345678, Accuracy = 0.9854, }, - new OnlineScore + new APIScore { User = new User { @@ -279,7 +279,7 @@ namespace osu.Game.Tests.Visual s.Statistics.Add(HitResult.Meh, RNG.Next(2000)); } - topScore = new OnlineScore + topScore = new APIScore { User = new User { diff --git a/osu.Game.Tests/Visual/TestCaseChatLink.cs b/osu.Game.Tests/Visual/TestCaseChatLink.cs index 89b1c52010..12a7ee9c12 100644 --- a/osu.Game.Tests/Visual/TestCaseChatLink.cs +++ b/osu.Game.Tests/Visual/TestCaseChatLink.cs @@ -35,9 +35,6 @@ namespace osu.Game.Tests.Visual typeof(MessageFormatter) }; - private DependencyContainer dependencies; - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent); - public TestCaseChatLink() { Add(textContainer = new TestChatLineContainer @@ -53,7 +50,7 @@ namespace osu.Game.Tests.Visual private void load(OsuColour colours) { linkColour = colours.Blue; - dependencies.Cache(new ChatOverlay + Dependencies.Cache(new ChatOverlay { AvailableChannels = { diff --git a/osu.Game.Tests/Visual/TestCaseCursors.cs b/osu.Game.Tests/Visual/TestCaseCursors.cs index 977f241f7a..0aa8e9691e 100644 --- a/osu.Game.Tests/Visual/TestCaseCursors.cs +++ b/osu.Game.Tests/Visual/TestCaseCursors.cs @@ -19,12 +19,12 @@ namespace osu.Game.Tests.Visual [TestFixture] public class TestCaseCursors : ManualInputManagerTestCase { - private readonly CursorOverrideContainer cursorOverrideContainer; + private readonly MenuCursorContainer menuCursorContainer; private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6]; public TestCaseCursors() { - Child = cursorOverrideContainer = new CursorOverrideContainer + Child = menuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both, Children = new[] @@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor)); AddStep("Move out", moveOut); AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor)); - AddAssert("Check global cursor visible", () => checkVisible(cursorOverrideContainer.Cursor)); + AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor)); } /// @@ -112,11 +112,11 @@ namespace osu.Game.Tests.Visual AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3])); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor)); AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor)); - AddAssert("Check global cursor visible", () => checkVisible(cursorOverrideContainer.Cursor)); - AddAssert("Check global cursor at mouse", () => checkAtMouse(cursorOverrideContainer.Cursor)); + AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor)); + AddAssert("Check global cursor at mouse", () => checkAtMouse(menuCursorContainer.Cursor)); AddStep("Move out", moveOut); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor)); - AddAssert("Check global cursor visible", () => checkVisible(cursorOverrideContainer.Cursor)); + AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor)); } /// diff --git a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs index bb5bf93a69..8fd8880fd6 100644 --- a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs +++ b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Screens.Multi.Components; @@ -111,6 +112,7 @@ namespace osu.Game.Tests.Visual } }); + AddStep(@"select", () => first.State = SelectionState.Selected); AddStep(@"change title", () => first.Room.Name.Value = @"I Changed Name"); AddStep(@"change host", () => first.Room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }); AddStep(@"change status", () => first.Room.Status.Value = new RoomStatusPlaying()); @@ -121,6 +123,7 @@ namespace osu.Game.Tests.Visual new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 1254 } } }, new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 123189 } } }, }); + AddStep(@"deselect", () => first.State = SelectionState.NotSelected); } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs index 96a754a5ce..e7bcfbf500 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs @@ -17,14 +17,10 @@ namespace osu.Game.Tests.Visual public override IReadOnlyList RequiredTypes => new[] { typeof(Compose) }; [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) + private void load() { - osuGame.Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo); - - var compose = new Compose(); - compose.Beatmap.BindTo(osuGame.Beatmap); - - Child = compose; + Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo); + Child = new Compose(); } } } diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs index a5053bafe8..9ad8bf7b92 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs @@ -4,45 +4,137 @@ using System; using System.Collections.Generic; using NUnit.Framework; -using OpenTK; using osu.Framework.Allocation; +using osu.Framework.Configuration; +using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Overlays; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Timing; +using osu.Game.Beatmaps; using osu.Game.Screens.Edit.Screens.Compose.Timeline; +using OpenTK.Graphics; namespace osu.Game.Tests.Visual { [TestFixture] - public class TestCaseEditorComposeTimeline : OsuTestCase + public class TestCaseEditorComposeTimeline : EditorClockTestCase { - public override IReadOnlyList RequiredTypes => new[] { typeof(ScrollableTimeline), typeof(ScrollingTimelineContainer), typeof(BeatmapWaveformGraph), typeof(TimelineButton) }; - - private readonly ScrollableTimeline timeline; - - public TestCaseEditorComposeTimeline() + public override IReadOnlyList RequiredTypes => new[] { + typeof(TimelineArea), + typeof(Timeline), + typeof(TimelineButton), + typeof(CentreMarker) + }; + + [BackgroundDependencyLoader] + private void load() + { + Beatmap.Value = new WaveformTestBeatmap(); + Children = new Drawable[] { - new MusicController + new FillFlowContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - State = Visibility.Visible + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + new StartStopButton(), + new AudioVisualiser(), + } }, - timeline = new ScrollableTimeline + new TimelineArea { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(1000, 100) + RelativeSizeAxes = Axes.X, + Size = new Vector2(0.8f, 100) } }; } - [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) + private class AudioVisualiser : CompositeDrawable { - timeline.Beatmap.BindTo(osuGame.Beatmap); + private readonly Drawable marker; + + private readonly IBindable beatmap = new Bindable(); + private IAdjustableClock adjustableClock; + + public AudioVisualiser() + { + Size = new Vector2(250, 25); + + InternalChildren = new[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.25f, + }, + marker = new Box + { + RelativePositionAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Width = 2, + } + }; + } + + [BackgroundDependencyLoader] + private void load(IAdjustableClock adjustableClock, IBindableBeatmap beatmap) + { + this.adjustableClock = adjustableClock; + this.beatmap.BindTo(beatmap); + } + + protected override void Update() + { + base.Update(); + + if (beatmap.Value.Track.IsLoaded) + marker.X = (float)(adjustableClock.CurrentTime / beatmap.Value.Track.Length); + } + } + + private class StartStopButton : Button + { + private IAdjustableClock adjustableClock; + private bool started; + + public StartStopButton() + { + BackgroundColour = Color4.SlateGray; + Size = new Vector2(100, 50); + Text = "Start"; + + Action = onClick; + } + + [BackgroundDependencyLoader] + private void load(IAdjustableClock adjustableClock) + { + this.adjustableClock = adjustableClock; + } + + private void onClick() + { + if (started) + { + adjustableClock.Stop(); + Text = "Start"; + } + else + { + adjustableClock.Start(); + Text = "Stop"; + } + + started = !started; + } } } } diff --git a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs index f037d70493..94b99d483c 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual } [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) + private void load() { var testBeatmap = new Beatmap { @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual } }; - osuGame.Beatmap.Value = new TestWorkingBeatmap(testBeatmap); + Beatmap.Value = new TestWorkingBeatmap(testBeatmap); Child = new TimingPointVisualiser(testBeatmap, 5000) { Clock = Clock }; diff --git a/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs index d01c2d2b92..cafd1b6f1a 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs @@ -19,19 +19,16 @@ namespace osu.Game.Tests.Visual public override IReadOnlyList RequiredTypes => new[] { typeof(SummaryTimeline) }; [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) + private void load() { - osuGame.Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo); + Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo); - SummaryTimeline summaryTimeline; - Add(summaryTimeline = new SummaryTimeline + Add(new SummaryTimeline { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(500, 50) }); - - summaryTimeline.Beatmap.BindTo(osuGame.Beatmap); } } } diff --git a/osu.Game.Tests/Visual/TestCaseExternalLinkButton.cs b/osu.Game.Tests/Visual/TestCaseExternalLinkButton.cs new file mode 100644 index 0000000000..7d8535f428 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseExternalLinkButton.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Game.Graphics.UserInterface; +using OpenTK; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseExternalLinkButton : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(ExternalLinkButton) }; + + public TestCaseExternalLinkButton() + { + Child = new ExternalLinkButton("https://osu.ppy.sh/home") + { + Size = new Vector2(50) + }; + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs b/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs index d0c46ecdd7..5df371dd09 100644 --- a/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs @@ -32,15 +32,10 @@ namespace osu.Game.Tests.Visual typeof(NotNullAttribute) }; - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(parent); - [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) + private void load() { - osuGame.Beatmap.Value = new TestWorkingBeatmap(new Beatmap + Beatmap.Value = new TestWorkingBeatmap(new Beatmap { HitObjects = new List { @@ -63,8 +58,8 @@ namespace osu.Game.Tests.Visual }); var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - dependencies.CacheAs(clock); - dependencies.CacheAs(clock); + Dependencies.CacheAs(clock); + Dependencies.CacheAs(clock); Child = new OsuHitObjectComposer(new OsuRuleset()); } diff --git a/osu.Game.Tests/Visual/TestCaseLoaderAnimation.cs b/osu.Game.Tests/Visual/TestCaseLoaderAnimation.cs new file mode 100644 index 0000000000..600784f8db --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseLoaderAnimation.cs @@ -0,0 +1,115 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Game.Screens; +using osu.Game.Screens.Menu; +using OpenTK.Graphics; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseLoaderAnimation : OsuTestCase + { + private TestLoader loader; + + protected override void LoadComplete() + { + base.LoadComplete(); + + // required to preload the logo in a headless run (so it doesn't delay the loading itself). + Add(new OsuLogo()); + + bool logoVisible = false; + AddStep("almost instant display", () => Child = loader = new TestLoader(250)); + AddUntilStep(() => + { + logoVisible = loader.Logo?.Alpha > 0; + return loader.Logo != null && loader.ScreenLoaded; + }, "loaded"); + AddAssert("logo not visible", () => !logoVisible); + + AddStep("short load", () => Child = loader = new TestLoader(800)); + AddUntilStep(() => + { + logoVisible = loader.Logo?.Alpha > 0; + return loader.Logo != null && loader.ScreenLoaded; + }, "loaded"); + AddAssert("logo visible", () => logoVisible); + AddUntilStep(() => loader.Logo?.Alpha == 0, "logo gone"); + + AddStep("longer load", () => Child = loader = new TestLoader(1400)); + AddUntilStep(() => + { + logoVisible = loader.Logo?.Alpha > 0; + return loader.Logo != null && loader.ScreenLoaded; + }, "loaded"); + AddAssert("logo visible", () => logoVisible); + AddUntilStep(() => loader.Logo?.Alpha == 0, "logo gone"); + } + + private class TestLoader : Loader + { + private readonly double delay; + + public OsuLogo Logo; + private TestScreen screen; + + public bool ScreenLoaded => screen.IsCurrentScreen; + + public TestLoader(double delay) + { + this.delay = delay; + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + Logo = logo; + base.LogoArriving(logo, resuming); + } + + protected override OsuScreen CreateLoadableScreen() => screen = new TestScreen(); + protected override ShaderPrecompiler CreateShaderPrecompiler() => new TestShaderPrecompiler(delay); + + private class TestShaderPrecompiler : ShaderPrecompiler + { + private readonly double delay; + private double startTime; + + public TestShaderPrecompiler(double delay) + { + this.delay = delay; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + startTime = Time.Current; + } + + protected override bool AllLoaded => Time.Current > startTime + delay; + } + + private class TestScreen : OsuScreen + { + public TestScreen() + { + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.DarkSlateGray, + Alpha = 0, + }; + } + + protected override void LogoArriving(OsuLogo logo, bool resuming) + { + base.LogoArriving(logo, resuming); + Child.FadeInFromZero(200); + } + } + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseLounge.cs b/osu.Game.Tests/Visual/TestCaseLounge.cs new file mode 100644 index 0000000000..c5e0d1c4bf --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseLounge.cs @@ -0,0 +1,215 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens; +using osu.Game.Screens.Backgrounds; +using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.Multi.Screens.Lounge; +using osu.Game.Users; +using OpenTK.Input; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseLounge : ManualInputManagerTestCase + { + private TestLounge lounge; + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + lounge = new TestLounge(); + + Room[] rooms = + { + new Room + { + Name = { Value = @"Just Another Room" }, + Host = { Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } } }, + Status = { Value = new RoomStatusPlaying() }, + Availability = { Value = RoomAvailability.Public }, + Type = { Value = new GameTypeTagTeam() }, + Beatmap = + { + Value = new BeatmapInfo + { + StarDifficulty = 5.65, + Ruleset = rulesets.GetRuleset(0), + Metadata = new BeatmapMetadata + { + Title = @"Sidetracked Day (Short Ver.)", + Artist = @"VINXIS", + AuthorString = @"Hobbes2", + }, + BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh/beatmaps/767600/covers/cover.jpg?1526243446", + }, + }, + }, + } + }, + MaxParticipants = { Value = 10 }, + Participants = + { + Value = new[] + { + new User { Username = @"flyte", Id = 3103765, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 142 } } }, + new User { Username = @"Cookiezi", Id = 124493, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 546 } } }, + new User { Username = @"Angelsim", Id = 1777162, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 287 } } }, + new User { Username = @"Rafis", Id = 2558286, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 468 } } }, + new User { Username = @"hvick225", Id = 50265, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 325 } } }, + new User { Username = @"peppy", Id = 2, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 625 } } }, + } + } + }, + new Room + { + Name = { Value = @"Not Just Any Room" }, + Host = { Value = new User { Username = @"Monstrata", Id = 2706438, Country = new Country { FlagName = @"CA" } } }, + Status = { Value = new RoomStatusOpen() }, + Availability = { Value = RoomAvailability.FriendsOnly }, + Type = { Value = new GameTypeTeamVersus() }, + Beatmap = + { + Value = new BeatmapInfo + { + StarDifficulty = 2.73, + Ruleset = rulesets.GetRuleset(0), + Metadata = new BeatmapMetadata + { + Title = @"lit(var)", + Artist = @"kensuke ushio", + AuthorString = @"Monstrata", + }, + BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh/beatmaps/623972/covers/cover.jpg?1521167183", + }, + }, + }, + } + }, + Participants = + { + Value = new[] + { + new User { Username = @"Jeby", Id = 3136279, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 3497 } } }, + new User { Username = @"DualAkira", Id = 5220933, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 643 } } }, + new User { Username = @"Datenshi Yohane", Id = 7171857, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 10555 } } }, + } + } + }, + new Room + { + Name = { Value = @"room THE FINAL" }, + Host = { Value = new User { Username = @"Delis", Id = 1603923, Country = new Country { FlagName = @"JP" } } }, + Status = { Value = new RoomStatusPlaying() }, + Availability = { Value = RoomAvailability.Public }, + Type = { Value = new GameTypeTagTeam() }, + Beatmap = + { + Value = new BeatmapInfo + { + StarDifficulty = 4.48, + Ruleset = rulesets.GetRuleset(3), + Metadata = new BeatmapMetadata + { + Title = @"ONIGIRI FREEWAY", + Artist = @"OISHII", + AuthorString = @"Mentholzzz", + }, + BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh/beatmaps/663098/covers/cover.jpg?1521898837", + }, + }, + }, + } + }, + MaxParticipants = { Value = 30 }, + Participants = + { + Value = new[] + { + new User { Username = @"KizuA", Id = 6510442, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 5372 } } }, + new User { Username = @"Colored", Id = 827563, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 810 } } }, + new User { Username = @"Beryl", Id = 3817591, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 10096 } } }, + } + } + }, + }; + + AddStep(@"show", () => Add(lounge)); + AddStep(@"set rooms", () => lounge.Rooms = rooms); + selectAssert(0); + AddStep(@"clear rooms", () => lounge.Rooms = new Room[] {}); + AddAssert(@"no room selected", () => lounge.SelectedRoom == null); + AddStep(@"set rooms", () => lounge.Rooms = rooms); + selectAssert(1); + AddStep(@"open room 1", () => clickRoom(1)); + AddStep(@"make lounge current", lounge.MakeCurrent); + filterAssert(@"THE FINAL", LoungeTab.Public, 1); + filterAssert(string.Empty, LoungeTab.Public, 2); + filterAssert(string.Empty, LoungeTab.Private, 1); + filterAssert(string.Empty, LoungeTab.Public, 2); + filterAssert(@"no matches", LoungeTab.Public, 0); + AddStep(@"clear rooms", () => lounge.Rooms = new Room[] {}); + AddStep(@"set rooms", () => lounge.Rooms = rooms); + AddAssert(@"no matches after clear", () => !lounge.ChildRooms.Any()); + filterAssert(string.Empty, LoungeTab.Public, 2); + AddStep(@"exit", lounge.Exit); + } + + private void clickRoom(int n) + { + InputManager.MoveMouseTo(lounge.ChildRooms.ElementAt(n)); + InputManager.Click(MouseButton.Left); + } + + private void selectAssert(int n) + { + AddStep($@"select room {n}", () => clickRoom(n)); + AddAssert($@"room {n} selected", () => lounge.SelectedRoom == lounge.ChildRooms.ElementAt(n).Room); + } + + private void filterAssert(string filter, LoungeTab tab, int endCount) + { + AddStep($@"filter '{filter}', {tab}", () => lounge.SetFilter(filter, tab)); + AddAssert(@"filtered correctly", () => lounge.ChildRooms.Count() == endCount); + } + + private class TestLounge : Lounge + { + protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); + + public IEnumerable ChildRooms => RoomsContainer.Children.Where(r => r.MatchingFilter); + public Room SelectedRoom => Inspector.Room; + + public void SetFilter(string filter, LoungeTab tab) + { + Filter.Search.Current.Value = filter; + Filter.Tabs.Current.Value = tab; + } + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMatch.cs b/osu.Game.Tests/Visual/TestCaseMatch.cs new file mode 100644 index 0000000000..bb22358425 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMatch.cs @@ -0,0 +1,142 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens.Multi.Screens.Match; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseMatch : OsuTestCase + { + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + Room room = new Room + { + Name = { Value = @"One Awesome Room" }, + Status = { Value = new RoomStatusOpen() }, + Availability = { Value = RoomAvailability.Public }, + Type = { Value = new GameTypeTeamVersus() }, + Beatmap = + { + Value = new BeatmapInfo + { + StarDifficulty = 5.02, + Ruleset = rulesets.GetRuleset(1), + Metadata = new BeatmapMetadata + { + Title = @"Paradigm Shift", + Artist = @"Morimori Atsushi", + AuthorString = @"eiri-", + }, + BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh/beatmaps/765055/covers/cover.jpg?1526955337", + }, + }, + }, + }, + }, + MaxParticipants = { Value = 5 }, + Participants = + { + Value = new[] + { + new User + { + Username = @"eiri-", + Id = 3388410, + Country = new Country { FlagName = @"US" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/3388410/00a8486a247831e1cc4375db519f611ac970bda8bc0057d78b0f540ea38c3e58.jpeg", + IsSupporter = true, + }, + new User + { + Username = @"Nepuri", + Id = 6637817, + Country = new Country { FlagName = @"DE" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/6637817/9085fc60248b6b5327a72c1dcdecf2dbedba810ae0ab6bcf7224e46b1339632a.jpeg", + IsSupporter = true, + }, + new User + { + Username = @"goheegy", + Id = 8057655, + Country = new Country { FlagName = @"GB" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8057655/21cec27c25a11dc197a4ec6a74253dbabb495949b0e0697113352f12007018c5.jpeg", + }, + new User + { + Username = @"Alumetri", + Id = 5371497, + Country = new Country { FlagName = @"RU" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/5371497/e023b8c7fbe3613e64bd4856703517ea50fbed8a5805dc9acda9efe9897c67e2.jpeg", + }, + } + }, + }; + + Match match = new Match(room); + + AddStep(@"show", () => Add(match)); + AddStep(@"null beatmap", () => room.Beatmap.Value = null); + AddStep(@"change name", () => room.Name.Value = @"Two Awesome Rooms"); + AddStep(@"change status", () => room.Status.Value = new RoomStatusPlaying()); + AddStep(@"change availability", () => room.Availability.Value = RoomAvailability.FriendsOnly); + AddStep(@"change type", () => room.Type.Value = new GameTypeTag()); + AddStep(@"change beatmap", () => room.Beatmap.Value = new BeatmapInfo + { + StarDifficulty = 4.33, + Ruleset = rulesets.GetRuleset(2), + Metadata = new BeatmapMetadata + { + Title = @"Yasashisa no Riyuu", + Artist = @"ChouCho", + AuthorString = @"celerih", + }, + BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh/beatmaps/685391/covers/cover.jpg?1524597970", + }, + }, + }, + }); + + AddStep(@"null max participants", () => room.MaxParticipants.Value = null); + AddStep(@"change participants", () => room.Participants.Value = new[] + { + new User + { + Username = @"Spectator", + Id = 702598, + Country = new Country { FlagName = @"KR" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/702598/3bbf4cb8b8d2cf8b03145000a975ff27e191ab99b0920832e7dd67386280e288.jpeg", + IsSupporter = true, + }, + new User + { + Username = @"celerih", + Id = 4696296, + Country = new Country { FlagName = @"CA" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/4696296/7f8500731d0ac66d5472569d146a7be07d9460273361913f22c038867baddaef.jpeg", + }, + }); + + AddStep(@"exit", match.Exit); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMatchHeader.cs b/osu.Game.Tests/Visual/TestCaseMatchHeader.cs new file mode 100644 index 0000000000..34f98f97c2 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMatchHeader.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Screens.Multi.Screens.Match; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseMatchHeader : OsuTestCase + { + public TestCaseMatchHeader() + { + Header header = new Header(); + Add(header); + + AddStep(@"set beatmap set", () => header.BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh/beatmaps/760757/covers/cover.jpg?1526944540", + }, + }, + }); + + AddStep(@"change beatmap set", () => header.BeatmapSet = new BeatmapSetInfo + { + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = @"https://assets.ppy.sh/beatmaps/761883/covers/cover.jpg?1525557400", + }, + }, + }); + + AddStep(@"null beatmap set", () => header.BeatmapSet = null); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMatchInfo.cs b/osu.Game.Tests/Visual/TestCaseMatchInfo.cs new file mode 100644 index 0000000000..205da6932f --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMatchInfo.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens.Multi.Screens.Match; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseMatchInfo : OsuTestCase + { + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + Info info = new Info(); + Add(info); + + AddStep(@"set name", () => info.Name = @"Room Name?"); + AddStep(@"set availability", () => info.Availability = RoomAvailability.FriendsOnly); + AddStep(@"set status", () => info.Status = new RoomStatusPlaying()); + AddStep(@"set beatmap", () => info.Beatmap = new BeatmapInfo + { + StarDifficulty = 2.4, + Ruleset = rulesets.GetRuleset(0), + Metadata = new BeatmapMetadata + { + Title = @"My Song", + Artist = @"VisualTests", + AuthorString = @"osu!lazer", + }, + }); + + AddStep(@"set type", () => info.Type = new GameTypeTagTeam()); + + AddStep(@"change name", () => info.Name = @"Room Name!"); + AddStep(@"change availability", () => info.Availability = RoomAvailability.InviteOnly); + AddStep(@"change status", () => info.Status = new RoomStatusOpen()); + AddStep(@"null beatmap", () => info.Beatmap = null); + AddStep(@"change type", () => info.Type = new GameTypeTeamVersus()); + AddStep(@"change beatmap", () => info.Beatmap = new BeatmapInfo + { + StarDifficulty = 4.2, + Ruleset = rulesets.GetRuleset(3), + Metadata = new BeatmapMetadata + { + Title = @"Your Song", + Artist = @"Tester", + AuthorString = @"Someone", + }, + }); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMatchParticipants.cs b/osu.Game.Tests/Visual/TestCaseMatchParticipants.cs new file mode 100644 index 0000000000..d6ae07252b --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseMatchParticipants.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Screens.Multi.Screens.Match; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseMatchParticipants : OsuTestCase + { + public TestCaseMatchParticipants() + { + Participants participants; + Add(participants = new Participants + { + RelativeSizeAxes = Axes.Both, + }); + + AddStep(@"set max to null", () => participants.Max = null); + AddStep(@"set users", () => participants.Users = new[] + { + new User + { + Username = @"Feppla", + Id = 4271601, + Country = new Country { FlagName = @"SE" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + IsSupporter = true, + }, + new User + { + Username = @"Xilver", + Id = 3099689, + Country = new Country { FlagName = @"IL" }, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", + IsSupporter = true, + }, + new User + { + Username = @"Wucki", + Id = 5287410, + Country = new Country { FlagName = @"FI" }, + CoverUrl = @"https://assets.ppy.sh/user-profile-covers/5287410/5cfeaa9dd41cbce038ecdc9d781396ed4b0108089170bf7f50492ef8eadeb368.jpeg", + IsSupporter = true, + }, + }); + + AddStep(@"set max", () => participants.Max = 10); + AddStep(@"clear users", () => participants.Users = new User[] { }); + AddStep(@"set max to null", () => participants.Max = null); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseMods.cs b/osu.Game.Tests/Visual/TestCaseMods.cs index d3d21509fd..3255478bea 100644 --- a/osu.Game.Tests/Visual/TestCaseMods.cs +++ b/osu.Game.Tests/Visual/TestCaseMods.cs @@ -39,8 +39,6 @@ namespace osu.Game.Tests.Visual typeof(SpecialSection), }; - private const string unranked_suffix = " (Unranked)"; - private RulesetStore rulesets; private ModDisplay modDisplay; private TestModSelectOverlay modSelect; @@ -121,7 +119,7 @@ namespace osu.Game.Tests.Visual private void testManiaMods(ManiaRuleset ruleset) { - testMultiplierTextUnranked(ruleset.GetModsFor(ModType.Special).First(m => m is ManiaModRandom)); + testRankedText(ruleset.GetModsFor(ModType.Special).First(m => m is ManiaModRandom)); } private void testSingleMod(Mod mod) @@ -198,13 +196,16 @@ namespace osu.Game.Tests.Visual checkLabelColor(Color4.White); } - private void testMultiplierTextUnranked(Mod mod) + private void testRankedText(Mod mod) { - AddAssert("check for ranked", () => !modSelect.MultiplierLabel.Text.EndsWith(unranked_suffix)); + AddWaitStep(1, "wait for fade"); + AddAssert("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0); selectNext(mod); - AddAssert("check for unranked", () => modSelect.MultiplierLabel.Text.EndsWith(unranked_suffix)); + AddWaitStep(1, "wait for fade"); + AddAssert("check for unranked", () => modSelect.UnrankedLabel.Alpha != 0); selectPrevious(mod); - AddAssert("check for ranked", () => !modSelect.MultiplierLabel.Text.EndsWith(unranked_suffix)); + AddWaitStep(1, "wait for fade"); + AddAssert("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0); } private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1)); @@ -240,6 +241,7 @@ namespace osu.Game.Tests.Visual } public new OsuSpriteText MultiplierLabel => base.MultiplierLabel; + public new OsuSpriteText UnrankedLabel => base.UnrankedLabel; public new TriangleButton DeselectAllButton => base.DeselectAllButton; public new Color4 LowMultiplierColour => base.LowMultiplierColour; diff --git a/osu.Game.Tests/Visual/TestCaseMultiHeader.cs b/osu.Game.Tests/Visual/TestCaseMultiHeader.cs index af51a6221f..d27a447077 100644 --- a/osu.Game.Tests/Visual/TestCaseMultiHeader.cs +++ b/osu.Game.Tests/Visual/TestCaseMultiHeader.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Screens; +using osu.Game.Screens.Multi.Screens.Lounge; namespace osu.Game.Tests.Visual { @@ -13,14 +13,14 @@ namespace osu.Game.Tests.Visual { public TestCaseMultiHeader() { - Lobby lobby; + Lounge lounge; Children = new Drawable[] { - lobby = new Lobby + lounge = new Lounge { Padding = new MarginPadding { Top = Header.HEIGHT }, }, - new Header(lobby), + new Header(lounge), }; } } diff --git a/osu.Game.Tests/Visual/TestCaseMusicController.cs b/osu.Game.Tests/Visual/TestCaseMusicController.cs index 10c813b2f8..5ba0167f12 100644 --- a/osu.Game.Tests/Visual/TestCaseMusicController.cs +++ b/osu.Game.Tests/Visual/TestCaseMusicController.cs @@ -2,12 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; -using osu.Game.Beatmaps; using osu.Game.Overlays; namespace osu.Game.Tests.Visual @@ -15,8 +12,6 @@ namespace osu.Game.Tests.Visual [TestFixture] public class TestCaseMusicController : OsuTestCase { - private readonly Bindable beatmapBacking = new Bindable(); - public TestCaseMusicController() { Clock = new FramedClock(); @@ -30,13 +25,7 @@ namespace osu.Game.Tests.Visual AddToggleStep(@"toggle visibility", state => mc.State = state ? Visibility.Visible : Visibility.Hidden); AddStep(@"show", () => mc.State = Visibility.Visible); - AddToggleStep(@"toggle beatmap lock", state => beatmapBacking.Disabled = state); - } - - [BackgroundDependencyLoader] - private void load(OsuGameBase game) - { - beatmapBacking.BindTo(game.Beatmap); + AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state); } } } diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 8c52360db8..dab7f7e037 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -27,7 +27,6 @@ namespace osu.Game.Tests.Visual private RulesetStore rulesets; - private DependencyContainer dependencies; private WorkingBeatmap defaultBeatmap; public override IReadOnlyList RequiredTypes => new[] @@ -48,8 +47,6 @@ namespace osu.Game.Tests.Visual typeof(DrawableCarouselBeatmapSet), }; - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent); - private class TestSongSelect : PlaySongSelect { public WorkingBeatmap CurrentBeatmap => Beatmap.Value; @@ -58,7 +55,7 @@ namespace osu.Game.Tests.Visual } [BackgroundDependencyLoader] - private void load(OsuGameBase game) + private void load() { TestSongSelect songSelect = null; @@ -67,10 +64,10 @@ namespace osu.Game.Tests.Visual // this is by no means clean. should be replacing inside of OsuGameBase somehow. IDatabaseContextFactory factory = new SingletonContextFactory(new OsuDbContext()); - dependencies.Cache(rulesets = new RulesetStore(factory)); - dependencies.Cache(manager = new BeatmapManager(storage, factory, rulesets, null, null) + Dependencies.Cache(rulesets = new RulesetStore(factory)); + Dependencies.Cache(manager = new BeatmapManager(storage, factory, rulesets, null, null) { - DefaultBeatmap = defaultBeatmap = game.Beatmap.Default + DefaultBeatmap = defaultBeatmap = Beatmap.Default }); void loadNewSongSelect(bool deleteMaps = false) => AddStep("reload song select", () => @@ -78,7 +75,7 @@ namespace osu.Game.Tests.Visual if (deleteMaps) { manager.Delete(manager.GetAllUsableBeatmapSets()); - game.Beatmap.SetDefault(); + Beatmap.SetDefault(); } if (songSelect != null) @@ -125,7 +122,6 @@ namespace osu.Game.Tests.Visual Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), Metadata = new BeatmapMetadata { - OnlineBeatmapSetID = 1234 + i, // Create random metadata, then we can check if sorting works based on these Artist = "MONACA " + RNG.Next(0, 9), Title = "Black Song " + RNG.Next(0, 9), diff --git a/osu.Game.Tests/Visual/TestCasePlaybackControl.cs b/osu.Game.Tests/Visual/TestCasePlaybackControl.cs index 24ebb534c1..36fb1bcedd 100644 --- a/osu.Game.Tests/Visual/TestCasePlaybackControl.cs +++ b/osu.Game.Tests/Visual/TestCasePlaybackControl.cs @@ -15,17 +15,12 @@ namespace osu.Game.Tests.Visual [TestFixture] public class TestCasePlaybackControl : OsuTestCase { - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(parent); - [BackgroundDependencyLoader] private void load() { var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; - dependencies.CacheAs(clock); - dependencies.CacheAs(clock); + Dependencies.CacheAs(clock); + Dependencies.CacheAs(clock); var playback = new PlaybackControl { @@ -34,7 +29,7 @@ namespace osu.Game.Tests.Visual Size = new Vector2(200,100) }; - playback.Beatmap.Value = new TestWorkingBeatmap(new Beatmap()); + Beatmap.Value = new TestWorkingBeatmap(new Beatmap()); Child = playback; } diff --git a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs index 1e7618232d..52a9db080d 100644 --- a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs +++ b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs @@ -12,9 +12,10 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load(OsuGameBase game) { + Beatmap.Value = new DummyWorkingBeatmap(game); + AddStep("load dummy beatmap", () => Add(new PlayerLoader(new Player { - InitialBeatmap = new DummyWorkingBeatmap(game), AllowPause = false, AllowLeadIn = false, AllowResults = false, diff --git a/osu.Game.Tests/Visual/TestCaseQuitButton.cs b/osu.Game.Tests/Visual/TestCaseQuitButton.cs new file mode 100644 index 0000000000..a427b7a20a --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseQuitButton.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Screens.Play.HUD; +using OpenTK; +using OpenTK.Input; + +namespace osu.Game.Tests.Visual +{ + [Description("'Hold to Quit' UI element")] + public class TestCaseQuitButton : ManualInputManagerTestCase + { + private bool exitAction; + + [BackgroundDependencyLoader] + private void load() + { + QuitButton quitButton; + + Add(quitButton = new QuitButton + { + Origin = Anchor.BottomRight, + Anchor = Anchor.BottomRight, + Action = () => exitAction = true + }); + + var text = quitButton.Children.OfType().First(); + + AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(quitButton)); + AddUntilStep(() => text.IsPresent && !exitAction, "Text visible"); + AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One)); + AddUntilStep(() => !text.IsPresent && !exitAction, "Text is not visible"); + + AddStep("Trigger exit action", () => + { + exitAction = false; + InputManager.MoveMouseTo(quitButton); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("Early release", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("action not triggered", () => !exitAction); + + AddStep("Trigger exit action", () => InputManager.PressButton(MouseButton.Left)); + AddUntilStep(() => exitAction, $"{nameof(quitButton.Action)} was triggered"); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs index 5bc16fe420..1f2d99a7d8 100644 --- a/osu.Game.Tests/Visual/TestCaseReplay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplay.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.Linq; -using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; @@ -13,23 +12,20 @@ namespace osu.Game.Tests.Visual [Description("Player instantiated with a replay.")] public class TestCaseReplay : TestCasePlayer { - protected override Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) + protected override Player CreatePlayer(Ruleset ruleset) { // We create a dummy RulesetContainer just to get the replay - we don't want to use mods here // to simulate setting a replay rather than having the replay already set for us - beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); - var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap); + Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); + var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(Beatmap.Value); // We have the replay var replay = dummyRulesetContainer.Replay; // Reset the mods - beatmap.Mods.Value = beatmap.Mods.Value.Where(m => !(m is ModAutoplay)); + Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Where(m => !(m is ModAutoplay)); - return new ReplayPlayer(replay) - { - InitialBeatmap = beatmap - }; + return new ReplayPlayer(replay); } } } diff --git a/osu.Game.Tests/Visual/TestCaseResults.cs b/osu.Game.Tests/Visual/TestCaseResults.cs index 35e1db7c9e..ee36fb0afc 100644 --- a/osu.Game.Tests/Visual/TestCaseResults.cs +++ b/osu.Game.Tests/Visual/TestCaseResults.cs @@ -32,18 +32,13 @@ namespace osu.Game.Tests.Visual this.beatmaps = beatmaps; } - private WorkingBeatmap beatmap; - protected override void LoadComplete() { base.LoadComplete(); - if (beatmap == null) - { - var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); - if (beatmapInfo != null) - beatmap = beatmaps.GetWorkingBeatmap(beatmapInfo); - } + var beatmapInfo = beatmaps.QueryBeatmap(b => b.RulesetID == 0); + if (beatmapInfo != null) + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo); Add(new Results(new Score { @@ -63,10 +58,7 @@ namespace osu.Game.Tests.Visual { Username = "peppy", } - }) - { - InitialBeatmap = beatmap - }); + })); } } } diff --git a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs index cb1425ca69..06b9c4a6f9 100644 --- a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs +++ b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual { base.LoadComplete(); - var room = new Room + Room room = new Room { Name = { Value = @"My Awesome Room" }, Host = { Value = new User { Username = @"flyte", Id = 3103765, Country = new Country { FlagName = @"JP" } } }, @@ -71,9 +71,13 @@ namespace osu.Game.Tests.Visual { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Room = room, + RelativeSizeAxes = Axes.Both, + Width = 0.5f, }); + AddStep(@"set room", () => inspector.Room = room); + AddStep(@"null room", () => inspector.Room = null); + AddStep(@"set room", () => inspector.Room = room); AddStep(@"change title", () => room.Name.Value = @"A Better Room Than The Above"); AddStep(@"change host", () => room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } }); AddStep(@"change status", () => room.Status.Value = new RoomStatusPlaying()); @@ -88,7 +92,7 @@ namespace osu.Game.Tests.Visual AddStep(@"change room", () => { - var newRoom = new Room + Room newRoom = new Room { Name = { Value = @"My New, Better Than Ever Room" }, Host = { Value = new User { Username = @"Angelsim", Id = 1777162, Country = new Country { FlagName = @"KR" } } }, diff --git a/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs b/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs index 7a743655f4..83bbbfddd1 100644 --- a/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs +++ b/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs @@ -84,12 +84,9 @@ namespace osu.Game.Tests.Visual private abstract class TestScreen : OsuScreen { - protected abstract string Title { get; } protected abstract string NextTitle { get; } protected abstract TestScreen CreateNextScreen(); - public override string ToString() => Title; - public TestScreen PushNext() { TestScreen screen = CreateNextScreen(); @@ -130,14 +127,14 @@ namespace osu.Game.Tests.Visual private class TestScreenOne : TestScreen { - protected override string Title => @"Screen One"; + public override string Title => @"Screen One"; protected override string NextTitle => @"Two"; protected override TestScreen CreateNextScreen() => new TestScreenTwo(); } private class TestScreenTwo : TestScreen { - protected override string Title => @"Screen Two"; + public override string Title => @"Screen Two"; protected override string NextTitle => @"One"; protected override TestScreen CreateNextScreen() => new TestScreenOne(); } diff --git a/osu.Game.Tests/Visual/TestCaseSettings.cs b/osu.Game.Tests/Visual/TestCaseSettings.cs index 5dad48c6d7..c942342168 100644 --- a/osu.Game.Tests/Visual/TestCaseSettings.cs +++ b/osu.Game.Tests/Visual/TestCaseSettings.cs @@ -14,10 +14,6 @@ namespace osu.Game.Tests.Visual private readonly SettingsOverlay settings; private readonly DialogOverlay dialogOverlay; - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent); - public TestCaseSettings() { settings = new MainSettings @@ -33,7 +29,7 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { - dependencies.Cache(dialogOverlay); + Dependencies.Cache(dialogOverlay); Add(settings); } diff --git a/osu.Game.Tests/Visual/TestCaseStoryboard.cs b/osu.Game.Tests/Visual/TestCaseStoryboard.cs index e721c5ced0..b63881ffa7 100644 --- a/osu.Game.Tests/Visual/TestCaseStoryboard.cs +++ b/osu.Game.Tests/Visual/TestCaseStoryboard.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -18,8 +17,6 @@ namespace osu.Game.Tests.Visual [TestFixture] public class TestCaseStoryboard : OsuTestCase { - private readonly Bindable beatmapBacking = new Bindable(); - private readonly Container storyboardContainer; private DrawableStoryboard storyboard; @@ -43,6 +40,7 @@ namespace osu.Game.Tests.Visual }, }, }); + Add(new MusicController { Origin = Anchor.TopRight, @@ -55,10 +53,9 @@ namespace osu.Game.Tests.Visual } [BackgroundDependencyLoader] - private void load(OsuGameBase game) + private void load() { - beatmapBacking.BindTo(game.Beatmap); - beatmapBacking.ValueChanged += beatmapChanged; + Beatmap.ValueChanged += beatmapChanged; } private void beatmapChanged(WorkingBeatmap working) @@ -66,10 +63,10 @@ namespace osu.Game.Tests.Visual private void restart() { - var track = beatmapBacking.Value.Track; + var track = Beatmap.Value.Track; track.Reset(); - loadStoryboard(beatmapBacking.Value); + loadStoryboard(Beatmap); track.Start(); } @@ -81,7 +78,7 @@ namespace osu.Game.Tests.Visual var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; storyboardContainer.Clock = decoupledClock; - storyboard = working.Storyboard.CreateDrawable(beatmapBacking); + storyboard = working.Storyboard.CreateDrawable(Beatmap); storyboard.Passing = false; storyboardContainer.Add(storyboard); diff --git a/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs b/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs index f625bc0150..b14606780d 100644 --- a/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs +++ b/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs @@ -12,6 +12,7 @@ using osu.Game.Overlays.Profile.Sections.Recent; using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Tests.Visual { @@ -49,15 +50,15 @@ namespace osu.Game.Tests.Visual }; } - private IEnumerable createDummyActivities() + private IEnumerable createDummyActivities() { - var dummyBeatmap = new RecentActivity.RecentActivityBeatmap + var dummyBeatmap = new APIRecentActivity.RecentActivityBeatmap { Title = @"Dummy beatmap", Url = "/b/1337", }; - var dummyUser = new RecentActivity.RecentActivityUser + var dummyUser = new APIRecentActivity.RecentActivityUser { Username = "DummyReborn", Url = "/u/666", @@ -66,61 +67,61 @@ namespace osu.Game.Tests.Visual return new[] { - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.Achievement, - Achievement = new RecentActivity.RecentActivityAchievement + Achievement = new APIRecentActivity.RecentActivityAchievement { Name = @"Feelin' It", Slug = @"all-secret-feelinit", }, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.BeatmapPlaycount, Count = 1337, Beatmap = dummyBeatmap, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.BeatmapsetApprove, Approval = BeatmapApproval.Qualified, Beatmapset = dummyBeatmap, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.BeatmapsetDelete, Beatmapset = dummyBeatmap, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.BeatmapsetRevive, Beatmapset = dummyBeatmap, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.BeatmapsetRevive, Beatmapset = dummyBeatmap, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.BeatmapsetUpdate, Beatmapset = dummyBeatmap, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.BeatmapsetUpload, Beatmapset = dummyBeatmap, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.Rank, @@ -128,29 +129,29 @@ namespace osu.Game.Tests.Visual Mode = "osu!", Beatmap = dummyBeatmap, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.RankLost, Mode = "osu!", Beatmap = dummyBeatmap, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.UsernameChange, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.UserSupportAgain, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.UserSupportFirst, }, - new RecentActivity + new APIRecentActivity { User = dummyUser, Type = RecentActivityType.UserSupportGift, diff --git a/osu.Game.Tests/Visual/TestCaseVolumePieces.cs b/osu.Game.Tests/Visual/TestCaseVolumePieces.cs index 449f48b7d7..3c5b91ccd2 100644 --- a/osu.Game.Tests/Visual/TestCaseVolumePieces.cs +++ b/osu.Game.Tests/Visual/TestCaseVolumePieces.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Overlays.Volume; +using OpenTK; using OpenTK.Graphics; namespace osu.Game.Tests.Visual @@ -17,13 +18,21 @@ namespace osu.Game.Tests.Visual { VolumeMeter meter; MuteButton mute; - Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue)); + Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue) { Position = new Vector2(10) }); + AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1); + + Add(new VolumeMeter("BIG", 250, Color4.Red) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Position = new Vector2(10), + }); + Add(mute = new MuteButton { Margin = new MarginPadding { Top = 200 } }); - AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1); AddToggleStep("mute", b => mute.Current.Value = b); } } diff --git a/osu.Game.Tests/Visual/TestCaseWaveform.cs b/osu.Game.Tests/Visual/TestCaseWaveform.cs index 776adab0d1..46d46863ad 100644 --- a/osu.Game.Tests/Visual/TestCaseWaveform.cs +++ b/osu.Game.Tests/Visual/TestCaseWaveform.cs @@ -5,52 +5,39 @@ using NUnit.Framework; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Overlays; -using osu.Game.Screens.Edit.Screens.Compose.Timeline; namespace osu.Game.Tests.Visual { [TestFixture] public class TestCaseWaveform : OsuTestCase { - private readonly Bindable beatmapBacking = new Bindable(); - - public TestCaseWaveform() + [BackgroundDependencyLoader] + private void load() { + Beatmap.Value = new WaveformTestBeatmap(); + FillFlowContainer flow; Child = flow = new FillFlowContainer { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - new MusicController - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Y = 100, - State = Visibility.Visible - }, - } }; for (int i = 1; i <= 16; i *= 2) { - var newDisplay = new BeatmapWaveformGraph + var newDisplay = new WaveformGraph { RelativeSizeAxes = Axes.Both, - Resolution = 1f / i + Resolution = 1f / i, + Waveform = Beatmap.Value.Waveform, }; - newDisplay.Beatmap.BindTo(beatmapBacking); - flow.Add(new Container { RelativeSizeAxes = Axes.X, @@ -83,8 +70,5 @@ namespace osu.Game.Tests.Visual }); } } - - [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) => beatmapBacking.BindTo(osuGame.Beatmap); } } diff --git a/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs b/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs new file mode 100644 index 0000000000..8bd1b79a84 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs @@ -0,0 +1,129 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Framework.MathUtils; +using osu.Game.Graphics; +using osu.Game.Graphics.Cursor; +using osu.Game.Screens.Edit.Screens.Compose.Timeline; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseZoomableScrollContainer : ManualInputManagerTestCase + { + private readonly ZoomableScrollContainer scrollContainer; + private readonly Drawable innerBox; + + public TestCaseZoomableScrollContainer() + { + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 250, + Width = 0.75f, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(30) + }, + scrollContainer = new ZoomableScrollContainer { RelativeSizeAxes = Axes.Both } + } + }, + new MenuCursor() + }; + + scrollContainer.Add(innerBox = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0.8f, 0.6f, 0.4f, 1f), new Color4(0.4f, 0.6f, 0.8f, 1f)) + }); + } + + [Test] + public void TestZoom0() + { + reset(); + AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + AddAssert("Box width = 1x", () => Precision.AlmostEquals(boxQuad.Size, scrollQuad.Size)); + } + + [Test] + public void TestZoom10() + { + reset(); + AddStep("Set zoom = 10", () => scrollContainer.Zoom = 10); + AddAssert("Box at 1/2", () => Precision.AlmostEquals(boxQuad.Centre, scrollQuad.Centre, 1)); + AddAssert("Box width = 10x", () => Precision.AlmostEquals(boxQuad.Size.X, 10 * scrollQuad.Size.X)); + } + + [Test] + public void TestMouseZoomInOnceOutOnce() + { + reset(); + + // Scroll in at 0.25 + AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3))); + AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + + // Scroll out at 0.25 + AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3))); + AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + } + + [Test] + public void TestMouseZoomInTwiceOutTwice() + { + reset(); + + // Scroll in at 0.25 + AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(0, 1))); + + // Scroll in at 0.6 + AddStep("Move mouse to 0.75x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.75f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(0, 1))); + AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + + // Very hard to determine actual position, so approximate + AddAssert("Box at correct position (1)", () => Precision.DefinitelyBigger(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X)); + AddAssert("Box at correct position (2)", () => Precision.DefinitelyBigger(scrollQuad.TopLeft.X + 0.6f * scrollQuad.Size.X, boxQuad.TopLeft.X + 0.3f * boxQuad.Size.X)); + AddAssert("Box at correct position (3)", () => Precision.DefinitelyBigger(boxQuad.TopLeft.X + 0.6f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.6f * scrollQuad.Size.X)); + + // Scroll out at 0.6 + AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(0, -1))); + + // Scroll out at 0.25 + AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(0, -1))); + AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + } + + private void reset() + { + AddStep("Reset", () => + { + scrollContainer.Zoom = 0; + scrollContainer.ScrollTo(0, false); + }); + } + + private Quad scrollQuad => scrollContainer.ScreenSpaceDrawQuad; + private Quad boxQuad => innerBox.ScreenSpaceDrawQuad; + } +} diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs new file mode 100644 index 0000000000..17aa7db14d --- /dev/null +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.IO; +using System.Linq; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO.Archives; +using osu.Game.Tests.Beatmaps.IO; + +namespace osu.Game.Tests +{ + /// + /// A that is used for testcases that include waveforms. + /// + public class WaveformTestBeatmap : WorkingBeatmap + { + private readonly ZipArchiveReader reader; + private readonly FileStream stream; + + public WaveformTestBeatmap() + : base(new BeatmapInfo()) + { + stream = File.OpenRead(ImportBeatmapTest.TEST_OSZ_PATH); + reader = new ZipArchiveReader(stream); + } + + public override void Dispose() + { + base.Dispose(); + stream?.Dispose(); + reader?.Dispose(); + } + + protected override IBeatmap GetBeatmap() => createTestBeatmap(); + + protected override Texture GetBackground() => null; + + protected override Waveform GetWaveform() => new Waveform(getAudioStream()); + + protected override Track GetTrack() => new TrackBass(getAudioStream()); + + private Stream getAudioStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".mp3"))); + private Stream getBeatmapStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu"))); + + private Beatmap createTestBeatmap() + { + using (var beatmapStream = getBeatmapStream()) + using (var beatmapReader = new StreamReader(beatmapStream)) + return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader); + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 057c2c2de1..532915100b 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -2,7 +2,7 @@ WinExe - netcoreapp2.0;net471 + netcoreapp2.1;net471 diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 40d62103a8..3afc3c4d32 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -23,7 +23,6 @@ namespace osu.Game.Beatmaps public int BeatmapVersion; private int? onlineBeatmapID; - private int? onlineBeatmapSetID; [JsonProperty("id")] public int? OnlineBeatmapID @@ -32,19 +31,10 @@ namespace osu.Game.Beatmaps set { onlineBeatmapID = value > 0 ? value : null; } } - [JsonProperty("beatmapset_id")] - [NotMapped] - public int? OnlineBeatmapSetID - { - get { return onlineBeatmapSetID; } - set { onlineBeatmapSetID = value > 0 ? value : null; } - } - [JsonIgnore] public int BeatmapSetInfoID { get; set; } [Required] - [JsonIgnore] public BeatmapSetInfo BeatmapSet { get; set; } public BeatmapMetadata Metadata { get; set; } @@ -141,8 +131,8 @@ namespace osu.Game.Beatmaps (Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile; public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && - BeatmapSet.Hash == other.BeatmapSet.Hash && - (Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile; + BeatmapSet.Hash == other.BeatmapSet.Hash && + (Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile; /// /// Returns a shallow-clone of this . diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 36fde8a319..5e3b66646b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -83,10 +83,29 @@ namespace osu.Game.Beatmaps { model.Beatmaps = createBeatmapDifficulties(archive); - // remove metadata from difficulties where it matches the set foreach (BeatmapInfo b in model.Beatmaps) + { + // remove metadata from difficulties where it matches the set if (model.Metadata.Equals(b.Metadata)) b.Metadata = null; + + // by setting the model here, we can update the noline set id below. + b.BeatmapSet = model; + + fetchAndPopulateOnlineIDs(b); + } + + // check if a set already exists with the same online id, delete if it does. + if (model.OnlineBeatmapSetID != null) + { + var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID); + if (existingOnlineId != null) + { + Delete(existingOnlineId); + beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); + Logger.Log($"Found existing beatmap set with same OnlineBeatmapSetID ({model.OnlineBeatmapSetID}). It has been purged.", LoggingTarget.Database); + } + } } protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model) @@ -99,17 +118,6 @@ namespace osu.Game.Beatmaps return existingHashMatch; } - // check if a set already exists with the same online id - if (model.OnlineBeatmapSetID != null) - { - var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID); - if (existingOnlineId != null) - { - Delete(existingOnlineId); - beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); - } - } - return null; } @@ -169,7 +177,7 @@ namespace osu.Game.Beatmaps { if (error is OperationCanceledException) return; - downloadNotification.State = ProgressNotificationState.Completed; + downloadNotification.State = ProgressNotificationState.Cancelled; Logger.Error(error, "Beatmap download failed!"); currentDownloads.Remove(request); }; @@ -241,7 +249,13 @@ namespace osu.Game.Beatmaps /// Returns a list of all usable s. /// /// A list of available . - public List GetAllUsableBeatmapSets() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected).ToList(); + public List GetAllUsableBeatmapSets() => GetAllUsableBeatmapSetsEnumerable().ToList(); + + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public IQueryable GetAllUsableBeatmapSetsEnumerable() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected); /// /// Perform a lookup query on available s. @@ -299,22 +313,22 @@ namespace osu.Game.Beatmaps return hashable.ComputeSHA2Hash(); } - protected override BeatmapSetInfo CreateModel(ArchiveReader reader) + protected override BeatmapSetInfo CreateModel(ArchiveReader reader) { // let's make sure there are actually .osu files to import. string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); - if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in the map folder."); + if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in this beatmap archive."); - BeatmapMetadata metadata; + Beatmap beatmap; using (var stream = new StreamReader(reader.GetStream(mapName))) - metadata = Decoder.GetDecoder(stream).Decode(stream).Metadata; + beatmap = Decoder.GetDecoder(stream).Decode(stream); return new BeatmapSetInfo { - OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, + OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, Beatmaps = new List(), Hash = computeBeatmapSetHash(reader), - Metadata = metadata + Metadata = beatmap.Metadata }; } @@ -341,6 +355,10 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); + // check that no existing beatmap exists that is imported with the same online beatmap ID. if so, give it precedence. + if (beatmap.BeatmapInfo.OnlineBeatmapID.HasValue && QueryBeatmap(b => b.OnlineBeatmapID.Value == beatmap.BeatmapInfo.OnlineBeatmapID.Value) != null) + beatmap.BeatmapInfo.OnlineBeatmapID = null; + RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); beatmap.BeatmapInfo.Ruleset = ruleset; @@ -348,8 +366,7 @@ namespace osu.Game.Beatmaps if (ruleset != null) { // TODO: this should be done in a better place once we actually need to dynamically update it. - var converted = new DummyConversionBeatmap(beatmap).GetPlayableBeatmap(ruleset); - beatmap.BeatmapInfo.StarDifficulty = ruleset.CreateInstance().CreateDifficultyCalculator(converted).Calculate(); + beatmap.BeatmapInfo.StarDifficulty = ruleset.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating; } else beatmap.BeatmapInfo.StarDifficulty = 0; @@ -361,6 +378,40 @@ namespace osu.Game.Beatmaps return beatmapInfos; } + /// + /// Query the API to populate mising OnlineBeatmapID / OnlineBeatmapSetID properties. + /// + /// The beatmap to populate. + /// Whether to re-query if the provided beatmap already has populated values. + /// True if population was successful. + private bool fetchAndPopulateOnlineIDs(BeatmapInfo beatmap, bool force = false) + { + if (!force && beatmap.OnlineBeatmapID != null && beatmap.BeatmapSet.OnlineBeatmapSetID != null) + return true; + + Logger.Log("Attempting online lookup for IDs...", LoggingTarget.Database); + + try + { + var req = new GetBeatmapRequest(beatmap); + + req.Perform(api); + + var res = req.Result; + + Logger.Log($"Successfully mapped to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.", LoggingTarget.Database); + + beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmap.OnlineBeatmapID = res.OnlineBeatmapID; + return true; + } + catch (Exception e) + { + Logger.Log($"Failed ({e})", LoggingTarget.Database); + return false; + } + } + /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 34147c18d2..6c1bcd0531 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -17,16 +17,6 @@ namespace osu.Game.Beatmaps [JsonIgnore] public int ID { 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; } @@ -82,8 +72,7 @@ namespace osu.Game.Beatmaps if (other == null) return false; - return onlineBeatmapSetID == other.onlineBeatmapSetID - && Title == other.Title + return Title == other.Title && TitleUnicode == other.TitleUnicode && Artist == other.Artist && ArtistUnicode == other.ArtistUnicode diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index fa08c6cb68..ed8fbdbb26 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -22,18 +22,18 @@ namespace osu.Game.Beatmaps [NotMapped] public BeatmapSetOnlineInfo OnlineInfo { get; set; } - public double MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty); + public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0; [NotMapped] public bool DeletePending { get; set; } public string Hash { get; set; } - public string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(".osb"))?.Filename; + public string StoryboardFile => Files?.FirstOrDefault(f => f.Filename.EndsWith(".osb"))?.Filename; public List Files { get; set; } - public override string ToString() => Metadata.ToString(); + public override string ToString() => Metadata?.ToString() ?? base.ToString(); public bool Protected { get; set; } } diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs index a70caf0207..8f985306e3 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs @@ -36,6 +36,11 @@ namespace osu.Game.Beatmaps /// public bool HasVideo { get; set; } + /// + /// Whether or not this beatmap set has a storyboard. + /// + public bool HasStoryboard { get; set; } + /// /// The different sizes of cover art for this beatmap set. /// diff --git a/osu.Game/Beatmaps/BindableBeatmap.cs b/osu.Game/Beatmaps/BindableBeatmap.cs new file mode 100644 index 0000000000..34d9cd3d25 --- /dev/null +++ b/osu.Game/Beatmaps/BindableBeatmap.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Diagnostics; +using JetBrains.Annotations; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Configuration; + +namespace osu.Game.Beatmaps +{ + /// + /// A for the beatmap. + /// This should be used sparingly in-favour of . + /// + public abstract class BindableBeatmap : NonNullableBindable, IBindableBeatmap + { + private AudioManager audioManager; + private WorkingBeatmap lastBeatmap; + + protected BindableBeatmap(WorkingBeatmap defaultValue) + : base(defaultValue) + { + } + + /// + /// Registers an for s to be added to. + /// + /// The to register. + protected void RegisterAudioManager([NotNull] AudioManager audioManager) + { + if (this.audioManager != null) throw new InvalidOperationException($"Cannot register multiple {nameof(AudioManager)}s."); + + this.audioManager = audioManager; + + ValueChanged += registerAudioTrack; + + // If the track has changed prior to this being called, let's register it + if (Value != Default) + registerAudioTrack(Value); + } + + private void registerAudioTrack(WorkingBeatmap beatmap) + { + var trackLoaded = lastBeatmap?.TrackLoaded ?? false; + + // compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) + if (!trackLoaded || lastBeatmap?.Track != beatmap.Track) + { + if (trackLoaded) + { + Debug.Assert(lastBeatmap != null); + Debug.Assert(lastBeatmap.Track != null); + + lastBeatmap.RecycleTrack(); + } + + audioManager.Track.AddItem(beatmap.Track); + } + + lastBeatmap = beatmap; + } + + [NotNull] + IBindableBeatmap IBindableBeatmap.GetBoundCopy() => GetBoundCopy(); + + /// + /// Retrieve a new instance weakly bound to this . + /// If you are further binding to events of the retrieved , ensure a local reference is held. + /// + [NotNull] + public abstract BindableBeatmap GetBoundCopy(); + } +} diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetDownloader.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetDownloader.cs new file mode 100644 index 0000000000..6acb58e165 --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetDownloader.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; + +namespace osu.Game.Beatmaps.Drawables +{ + /// + /// A component to allow downloading of a beatmap set. Automatically handles state syncing between other instances. + /// + public class BeatmapSetDownloader : Component + { + private readonly BeatmapSetInfo set; + private readonly bool noVideo; + + private BeatmapManager beatmaps; + + /// + /// Whether the associated beatmap set has been downloading (by this instance or any other instance). + /// + public readonly BindableBool Downloaded = new BindableBool(); + + public BeatmapSetDownloader(BeatmapSetInfo set, bool noVideo = false) + { + this.set = set; + this.noVideo = noVideo; + } + + [BackgroundDependencyLoader] + private void load(BeatmapManager beatmaps) + { + this.beatmaps = beatmaps; + + beatmaps.ItemAdded += setAdded; + beatmaps.ItemRemoved += setRemoved; + + // initial value + if (set.OnlineBeatmapSetID != null) + Downloaded.Value = beatmaps.QueryBeatmapSets(s => s.OnlineBeatmapSetID == set.OnlineBeatmapSetID && !s.DeletePending).Any(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (beatmaps != null) + { + beatmaps.ItemAdded -= setAdded; + beatmaps.ItemRemoved -= setRemoved; + } + } + + /// + /// Begin downloading the associated beatmap set. + /// + /// True if downloading began. False if an existing download is active or completed. + public bool Download() + { + if (Downloaded.Value) + return false; + + if (beatmaps.GetExistingDownload(set) != null) + return false; + + beatmaps.Download(set, noVideo); + return true; + } + + private void setAdded(BeatmapSetInfo s) + { + if (s.OnlineBeatmapSetID == set.OnlineBeatmapSetID) + Downloaded.Value = true; + } + + private void setRemoved(BeatmapSetInfo s) + { + if (s.OnlineBeatmapSetID == set.OnlineBeatmapSetID) + Downloaded.Value = false; + } + } +} diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs new file mode 100644 index 0000000000..1976db907c --- /dev/null +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using OpenTK.Graphics; + +namespace osu.Game.Beatmaps.Drawables +{ + public class UpdateableBeatmapSetCover : Container + { + private Drawable displayedCover; + + private BeatmapSetInfo beatmapSet; + public BeatmapSetInfo BeatmapSet + { + get { return beatmapSet; } + set + { + if (value == beatmapSet) return; + beatmapSet = value; + + if (IsLoaded) + updateCover(); + } + } + + private BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover; + public BeatmapSetCoverType CoverType + { + get { return coverType; } + set + { + if (value == coverType) return; + coverType = value; + + if (IsLoaded) + updateCover(); + } + } + + public UpdateableBeatmapSetCover() + { + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateCover(); + } + + private void updateCover() + { + displayedCover?.FadeOut(400); + displayedCover?.Expire(); + displayedCover = null; + + if (beatmapSet != null) + { + Add(displayedCover = new DelayedLoadWrapper( + new BeatmapSetCover(beatmapSet, coverType) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), + }) + ); + } + } + } +} diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 8094abe5ed..265c6832b2 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -17,7 +17,7 @@ namespace osu.Game.Beatmaps { private readonly OsuGameBase game; - public DummyWorkingBeatmap(OsuGameBase game) + public DummyWorkingBeatmap(OsuGameBase game = null) : base(new BeatmapInfo { Metadata = new BeatmapMetadata @@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps protected override IBeatmap GetBeatmap() => new Beatmap(); - protected override Texture GetBackground() => game.Textures.Get(@"Backgrounds/bg4"); + protected override Texture GetBackground() => game?.Textures.Get(@"Backgrounds/bg4"); protected override Track GetTrack() => new TrackVirtual(); @@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap }; - public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => null; + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => null; public override string Description => "dummy"; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 2aee419d20..581207607a 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -8,7 +8,6 @@ using System.Linq; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Beatmaps.ControlPoints; -using osu.Framework; namespace osu.Game.Beatmaps.Formats { @@ -28,23 +27,18 @@ namespace osu.Game.Beatmaps.Formats AddDecoder(@"osu file format v", m => new LegacyBeatmapDecoder(int.Parse(m.Split('v').Last()))); } - /// - /// lazer's audio timings in general doesn't match stable. this is the result of user testing, albeit limited. - /// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - /// - public static int UniversalOffset => RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? -22 : 0; - /// /// Whether or not beatmap or runtime offsets should be applied. Defaults on; only disable for testing purposes. /// public bool ApplyOffsets = true; - private readonly int offset = UniversalOffset; + private readonly int offset; - public LegacyBeatmapDecoder(int version = LATEST_VERSION) : base(version) + public LegacyBeatmapDecoder(int version = LATEST_VERSION) + : base(version) { // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) - offset += FormatVersion < 5 ? 24 : 0; + offset = FormatVersion < 5 ? 24 : 0; } protected override void ParseStreamInto(StreamReader stream, Beatmap beatmap) @@ -142,6 +136,7 @@ namespace osu.Game.Beatmaps.Formats parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(); break; } + break; case @"LetterboxInBreaks": beatmap.BeatmapInfo.LetterboxInBreaks = int.Parse(pair.Value) == 1; @@ -214,8 +209,7 @@ namespace osu.Game.Beatmaps.Formats beatmap.BeatmapInfo.OnlineBeatmapID = int.Parse(pair.Value); break; case @"BeatmapSetID": - beatmap.BeatmapInfo.OnlineBeatmapSetID = int.Parse(pair.Value); - metadata.OnlineBeatmapSetID = int.Parse(pair.Value); + beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = int.Parse(pair.Value) }; break; } } diff --git a/osu.Game/Beatmaps/IBindableBeatmap.cs b/osu.Game/Beatmaps/IBindableBeatmap.cs new file mode 100644 index 0000000000..329c0b6a3c --- /dev/null +++ b/osu.Game/Beatmaps/IBindableBeatmap.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; + +namespace osu.Game.Beatmaps +{ + /// + /// Read-only interface for the beatmap. + /// + public interface IBindableBeatmap : IBindable + { + /// + /// Retrieve a new instance weakly bound to this . + /// If you are further binding to events of the retrieved , ensure a local reference is held. + /// + IBindableBeatmap GetBoundCopy(); + } +} diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 9c389bbb8f..4310d9b7df 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -13,7 +13,6 @@ using osu.Game.Storyboards; using osu.Framework.IO.File; using System.IO; using osu.Game.IO.Serialization; -using System.Diagnostics; using osu.Game.Rulesets; using osu.Game.Rulesets.UI; using osu.Game.Skinning; @@ -49,12 +48,13 @@ namespace osu.Game.Beatmaps /// /// Saves the . /// - public void Save() + /// The absolute path of the output file. + public string Save() { var path = FileSafety.GetTempPath(Guid.NewGuid().ToString().Replace("-", string.Empty) + ".json"); using (var sw = new StreamWriter(path)) sw.WriteLine(Beatmap.Serialize()); - Process.Start(path); + return path; } protected abstract IBeatmap GetBeatmap(); @@ -107,8 +107,14 @@ namespace osu.Game.Beatmaps IBeatmap converted = converter.Convert(); // Apply difficulty mods - foreach (var mod in Mods.Value.OfType()) - mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); + if (Mods.Value.Any(m => m is IApplicableToDifficulty)) + { + converted.BeatmapInfo = converted.BeatmapInfo.Clone(); + converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone(); + + foreach (var mod in Mods.Value.OfType()) + mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); + } // Post-process rulesetInstance.CreateBeatmapProcessor(converted)?.PostProcess(); diff --git a/osu.Game/Configuration/DatabasedConfigManager.cs b/osu.Game/Configuration/DatabasedConfigManager.cs index 0ef0589dff..0ede6de0f2 100644 --- a/osu.Game/Configuration/DatabasedConfigManager.cs +++ b/osu.Game/Configuration/DatabasedConfigManager.cs @@ -13,13 +13,13 @@ namespace osu.Game.Configuration { private readonly SettingsStore settings; - private readonly int variant; + private readonly int? variant; private readonly List databasedSettings; private readonly RulesetInfo ruleset; - protected DatabasedConfigManager(SettingsStore settings, RulesetInfo ruleset = null, int variant = 0) + protected DatabasedConfigManager(SettingsStore settings, RulesetInfo ruleset = null, int? variant = null) { this.settings = settings; this.ruleset = ruleset; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index b3082e49de..f7fe424aa9 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -5,6 +5,7 @@ using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Platform; using osu.Game.Overlays; +using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; namespace osu.Game.Configuration @@ -80,8 +81,12 @@ namespace osu.Game.Configuration Set(OsuSetting.FloatingComments, false); + Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised); + Set(OsuSetting.SpeedChangeVisualisation, SpeedChangeVisualisationMethod.Sequential); + Set(OsuSetting.IncreaseFirstObjectVisibility, true); + // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -144,6 +149,8 @@ namespace osu.Game.Configuration ScreenshotCaptureMenuCursor, SongSelectRightMouseScroll, BeatmapSkins, - BeatmapHitsounds + BeatmapHitsounds, + IncreaseFirstObjectVisibility, + ScoreDisplayMode } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index e04559d547..cbf0df3227 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.EntityFrameworkCore; +using osu.Framework.IO.File; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IO; @@ -56,13 +57,49 @@ namespace osu.Game.Database // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; + private readonly List cachedEvents = new List(); + + /// + /// Allows delaying of outwards events until an operation is confirmed (at a database level). + /// + private bool delayingEvents; + + /// + /// Begin delaying outwards events. + /// + private void delayEvents() => delayingEvents = true; + + /// + /// Flush delayed events and disable delaying. + /// + /// Whether the flushed events should be performed. + private void flushEvents(bool perform) + { + if (perform) + { + foreach (var a in cachedEvents) + a.Invoke(); + } + + cachedEvents.Clear(); + delayingEvents = false; + } + + private void handleEvent(Action a) + { + if (delayingEvents) + cachedEvents.Add(a); + else + a.Invoke(); + } + protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStore modelStore, IIpcHost importHost = null) { ContextFactory = contextFactory; ModelStore = modelStore; - ModelStore.ItemAdded += s => ItemAdded?.Invoke(s); - ModelStore.ItemRemoved += s => ItemRemoved?.Invoke(s); + ModelStore.ItemAdded += s => handleEvent(() => ItemAdded?.Invoke(s)); + ModelStore.ItemRemoved += s => handleEvent(() => ItemRemoved?.Invoke(s)); Files = new FileStore(contextFactory, storage); @@ -138,24 +175,56 @@ namespace osu.Game.Database /// The archive to be imported. public TModel Import(ArchiveReader archive) { - using (ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. + TModel item = null; + delayEvents(); + + try { - // create a new model (don't yet add to database) - var item = CreateModel(archive); + using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. + { + try + { + if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}"); - var existing = CheckForExisting(item); + // create a new model (don't yet add to database) + item = CreateModel(archive); - if (existing != null) return existing; + var existing = CheckForExisting(item); - item.Files = createFileInfos(archive, Files); + if (existing != null) + { + Logger.Log($"Found existing {typeof(TModel)} for {archive.Name} (ID {existing.ID}). Skipping import.", LoggingTarget.Database); + return existing; + } - Populate(item, archive); + item.Files = createFileInfos(archive, Files); - // import to store - ModelStore.Add(item); + Populate(item, archive); - return item; + // import to store + ModelStore.Add(item); + } + catch (Exception e) + { + write.Errors.Add(e); + throw; + } + } + + Logger.Log($"Import of {archive.Name} successfully completed!", LoggingTarget.Database); } + catch (Exception e) + { + Logger.Error(e, $"Import of {archive.Name} failed and has been rolled back.", LoggingTarget.Database); + item = null; + } + finally + { + // we only want to flush events after we've confirmed the write context didn't have any errors. + flushEvents(item != null); + } + + return item; } /// @@ -178,12 +247,8 @@ namespace osu.Game.Database /// The item to delete. public void Delete(TModel item) { - using (var usage = ContextFactory.GetForWrite()) + using (ContextFactory.GetForWrite()) { - var context = usage.Context; - - context.ChangeTracker.AutoDetectChangesEnabled = false; - // re-fetch the model on the import context. var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == item.ID); @@ -191,8 +256,6 @@ namespace osu.Game.Database if (ModelStore.Delete(foundModel)) Files.Dereference(foundModel.Files.Select(f => f.FileInfo).ToArray()); - - context.ChangeTracker.AutoDetectChangesEnabled = true; } } @@ -302,7 +365,7 @@ namespace osu.Game.Database using (Stream s = reader.GetStream(file)) fileInfos.Add(new TFileModel { - Filename = file, + Filename = FileSafety.PathSanitise(file), FileInfo = files.Add(s) }); diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index 71960303b5..5160239c38 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -1,7 +1,10 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Linq; using System.Threading; +using Microsoft.EntityFrameworkCore.Storage; using osu.Framework.Platform; namespace osu.Game.Database @@ -17,8 +20,12 @@ namespace osu.Game.Database private readonly object writeLock = new object(); private bool currentWriteDidWrite; + private bool currentWriteDidError; + private int currentWriteUsages; + private IDbContextTransaction currentWriteTransaction; + public DatabaseContextFactory(GameHost host) { this.host = host; @@ -35,14 +42,41 @@ namespace osu.Game.Database /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). /// This method may block if a write is already active on a different thread. /// + /// Whether to start a transaction for this write. /// A usage containing a usable context. - public DatabaseWriteUsage GetForWrite() + public DatabaseWriteUsage GetForWrite(bool withTransaction = true) { Monitor.Enter(writeLock); + OsuDbContext context; + + try + { + if (currentWriteTransaction == null && withTransaction) + { + // this mitigates the fact that changes on tracked entities will not be rolled back with the transaction by ensuring write operations are always executed in isolated contexts. + // if this results in sub-optimal efficiency, we may need to look into removing Database-level transactions in favour of running SaveChanges where we currently commit the transaction. + if (threadContexts.IsValueCreated) + recycleThreadContexts(); + + context = threadContexts.Value; + currentWriteTransaction = context.Database.BeginTransaction(); + } + else + { + // we want to try-catch the retrieval of the context because it could throw an error (in CreateContext). + context = threadContexts.Value; + } + } + catch (Exception e) + { + // retrieval of a context could trigger a fatal error. + Monitor.Exit(writeLock); + throw; + } Interlocked.Increment(ref currentWriteUsages); - return new DatabaseWriteUsage(threadContexts.Value, usageCompleted); + return new DatabaseWriteUsage(context, usageCompleted) { IsTransactionLeader = currentWriteTransaction != null && currentWriteUsages == 1 }; } private void usageCompleted(DatabaseWriteUsage usage) @@ -52,18 +86,27 @@ namespace osu.Game.Database try { currentWriteDidWrite |= usage.PerformedWrite; + currentWriteDidError |= usage.Errors.Any(); - if (usages > 0) return; - - if (currentWriteDidWrite) + if (usages == 0) { - // explicitly dispose to ensure any outstanding flushes happen as soon as possible (and underlying resources are purged). - usage.Context.Dispose(); + if (currentWriteDidError) + currentWriteTransaction?.Rollback(); + else + currentWriteTransaction?.Commit(); + if (currentWriteDidWrite || currentWriteDidError) + { + // explicitly dispose to ensure any outstanding flushes happen as soon as possible (and underlying resources are purged). + usage.Context.Dispose(); + + // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. + recycleThreadContexts(); + } + + currentWriteTransaction = null; currentWriteDidWrite = false; - - // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. - recycleThreadContexts(); + currentWriteDidError = false; } } finally @@ -74,19 +117,18 @@ namespace osu.Game.Database private void recycleThreadContexts() => threadContexts = new ThreadLocal(CreateContext); - protected virtual OsuDbContext CreateContext() + protected virtual OsuDbContext CreateContext() => new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name)) { - var ctx = new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name)); - ctx.Database.AutoTransactionsEnabled = false; - - return ctx; - } + Database = { AutoTransactionsEnabled = false } + }; public void ResetDatabase() { lock (writeLock) { recycleThreadContexts(); + GC.Collect(); + GC.WaitForPendingFinalizers(); host.Storage.DeleteDatabase(database_name); } } diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs index 7858c1a0d1..64ab24e824 100644 --- a/osu.Game/Database/DatabaseWriteUsage.cs +++ b/osu.Game/Database/DatabaseWriteUsage.cs @@ -2,34 +2,50 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using Microsoft.EntityFrameworkCore.Storage; +using System.Collections.Generic; namespace osu.Game.Database { public class DatabaseWriteUsage : IDisposable { public readonly OsuDbContext Context; - private readonly IDbContextTransaction transaction; private readonly Action usageCompleted; public DatabaseWriteUsage(OsuDbContext context, Action onCompleted) { Context = context; - transaction = Context.BeginTransaction(); usageCompleted = onCompleted; } public bool PerformedWrite { get; private set; } private bool isDisposed; + public List Errors = new List(); + + /// + /// Whether this write usage will commit a transaction on completion. + /// If false, there is a parent usage responsible for transaction commit. + /// + public bool IsTransactionLeader = false; protected void Dispose(bool disposing) { if (isDisposed) return; isDisposed = true; - PerformedWrite |= Context.SaveChanges(transaction) > 0; - usageCompleted?.Invoke(this); + try + { + PerformedWrite |= Context.SaveChanges() > 0; + } + catch (Exception e) + { + Errors.Add(e); + throw; + } + finally + { + usageCompleted?.Invoke(this); + } } public void Dispose() diff --git a/osu.Game/Database/IDatabaseContextFactory.cs b/osu.Game/Database/IDatabaseContextFactory.cs index 372e1770e4..d38d15b252 100644 --- a/osu.Game/Database/IDatabaseContextFactory.cs +++ b/osu.Game/Database/IDatabaseContextFactory.cs @@ -14,7 +14,8 @@ namespace osu.Game.Database /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). /// This method may block if a write is already active on a different thread. /// + /// Whether to start a transaction for this write. /// A usage containing a usable context. - DatabaseWriteUsage GetForWrite(); + DatabaseWriteUsage GetForWrite(bool withTransaction = true); } } diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs index 8569d81f01..69a1f57cc4 100644 --- a/osu.Game/Database/MutableDatabaseBackedStore.cs +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -50,11 +50,10 @@ namespace osu.Game.Database /// The item to update. public void Update(T item) { - ItemRemoved?.Invoke(item); - using (var usage = ContextFactory.GetForWrite()) usage.Context.Update(item); + ItemRemoved?.Invoke(item); ItemAdded?.Invoke(item); } diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 1979ce3648..bf57644caf 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -3,7 +3,6 @@ using System; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using osu.Framework.Logging; @@ -59,11 +58,20 @@ namespace osu.Game.Database this.connectionString = connectionString; var connection = Database.GetDbConnection(); - connection.Open(); - using (var cmd = connection.CreateCommand()) + try { - cmd.CommandText = "PRAGMA journal_mode=WAL;"; - cmd.ExecuteNonQuery(); + connection.Open(); + + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA journal_mode=WAL;"; + cmd.ExecuteNonQuery(); + } + } + catch (Exception e) + { + connection.Close(); + throw; } } @@ -83,8 +91,8 @@ namespace osu.Game.Database 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.MD5Hash); + modelBuilder.Entity().HasIndex(b => b.Hash); modelBuilder.Entity().HasIndex(b => b.OnlineBeatmapSetID).IsUnique(); modelBuilder.Entity().HasIndex(b => b.DeletePending); @@ -104,19 +112,6 @@ namespace osu.Game.Database modelBuilder.Entity().HasOne(b => b.BaseDifficulty); } - public IDbContextTransaction BeginTransaction() - { - // return Database.BeginTransaction(); - return null; - } - - public int SaveChanges(IDbContextTransaction transaction = null) - { - var ret = base.SaveChanges(); - if (ret > 0) transaction?.Commit(); - return ret; - } - private class OsuDbLoggerFactory : ILoggerFactory { #region Disposal @@ -186,24 +181,6 @@ namespace osu.Game.Database } } - public void Migrate() - { - try - { - Database.Migrate(); - } - catch (Exception e) - { - throw new MigrationFailedException(e); - } - } - } - - public class MigrationFailedException : Exception - { - public MigrationFailedException(Exception exception) - : base("sqlite-net migration failed", exception) - { - } + public void Migrate() => Database.Migrate(); } } diff --git a/osu.Game/Database/SingletonContextFactory.cs b/osu.Game/Database/SingletonContextFactory.cs index 74951e8433..ce3fbf6881 100644 --- a/osu.Game/Database/SingletonContextFactory.cs +++ b/osu.Game/Database/SingletonContextFactory.cs @@ -14,6 +14,6 @@ namespace osu.Game.Database public OsuDbContext Get() => context; - public DatabaseWriteUsage GetForWrite() => new DatabaseWriteUsage(context, null); + public DatabaseWriteUsage GetForWrite(bool withTransaction = true) => new DatabaseWriteUsage(context, null); } } diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index bf16af4706..f0d49af988 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -12,7 +12,7 @@ namespace osu.Game.Graphics.Containers { public class BeatSyncedContainer : Container { - protected readonly Bindable Beatmap = new Bindable(); + protected readonly IBindable Beatmap = new Bindable(); private int lastBeat; private TimingControlPoint lastTimingPoint; @@ -74,9 +74,9 @@ namespace osu.Game.Graphics.Containers } [BackgroundDependencyLoader] - private void load(OsuGameBase game) + private void load(IBindableBeatmap beatmap) { - Beatmap.BindTo(game.Beatmap); + Beatmap.BindTo(beatmap); } protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs new file mode 100644 index 0000000000..adfc258f61 --- /dev/null +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Graphics.Containers +{ + public abstract class HoldToConfirmContainer : Container + { + public Action Action; + + private const int activate_delay = 400; + private const int fadeout_delay = 200; + + private bool fired; + private bool confirming; + + /// + /// Whether the overlay should be allowed to return from a fired state. + /// + protected virtual bool AllowMultipleFires => false; + + public Bindable Progress = new BindableDouble(); + + protected void BeginConfirm() + { + if (confirming || !AllowMultipleFires && fired) return; + + confirming = true; + + this.TransformBindableTo(Progress, 1, activate_delay * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm()); + } + + protected virtual void Confirm() + { + Action?.Invoke(); + fired = true; + } + + protected void AbortConfirm() + { + if (!AllowMultipleFires && fired) return; + + confirming = false; + + this.TransformBindableTo(Progress, 0, fadeout_delay, Easing.Out); + } + } +} diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 157c814f55..9c5da71aff 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -3,11 +3,11 @@ using osu.Game.Online.Chat; using System; -using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; using System.Collections.Generic; +using osu.Framework.Platform; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -25,12 +25,14 @@ namespace osu.Game.Graphics.Containers private OsuGame game; private Action showNotImplementedError; + private GameHost host; [BackgroundDependencyLoader(true)] - private void load(OsuGame game, NotificationOverlay notifications) + private void load(OsuGame game, NotificationOverlay notifications, GameHost host) { // will be null in tests this.game = game; + this.host = host; showNotImplementedError = () => notifications?.Post(new SimpleNotification { @@ -88,7 +90,7 @@ namespace osu.Game.Graphics.Containers showNotImplementedError?.Invoke(); break; case LinkAction.External: - Process.Start(url); + host.OpenUrlExternally(url); break; case LinkAction.OpenUserProfile: if (long.TryParse(linkArgument, out long userId)) diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index f657c0cae5..0528f7b3ae 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -7,6 +7,8 @@ using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Containers; using osu.Framework.Input; using OpenTK; +using osu.Framework.Configuration; +using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { @@ -15,9 +17,14 @@ namespace osu.Game.Graphics.Containers private SampleChannel samplePopIn; private SampleChannel samplePopOut; - [BackgroundDependencyLoader] - private void load(AudioManager audio) + protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.All); + + [BackgroundDependencyLoader(true)] + private void load(OsuGame osuGame, AudioManager audio) { + if (osuGame != null) + OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); + samplePopIn = audio.Sample.Get(@"UI/overlay-pop-in"); samplePopOut = audio.Sample.Get(@"UI/overlay-pop-out"); @@ -26,7 +33,7 @@ namespace osu.Game.Graphics.Containers /// /// Whether mouse input should be blocked screen-wide while this overlay is visible. - /// Performing mouse actions outside of the valid extents will hide the overlay but pass the events through. + /// Performing mouse actions outside of the valid extents will hide the overlay. /// public virtual bool BlockScreenWideMouse => BlockPassThroughMouse; @@ -49,7 +56,10 @@ namespace osu.Game.Graphics.Containers switch (visibility) { case Visibility.Visible: - samplePopIn?.Play(); + if (OverlayActivationMode != OverlayActivation.Disabled) + samplePopIn?.Play(); + else + State = Visibility.Hidden; break; case Visibility.Hidden: samplePopOut?.Play(); diff --git a/osu.Game/Graphics/Cursor/CursorOverrideContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs similarity index 94% rename from osu.Game/Graphics/Cursor/CursorOverrideContainer.cs rename to osu.Game/Graphics/Cursor/MenuCursorContainer.cs index 1e56cb6052..5823fad93a 100644 --- a/osu.Game/Graphics/Cursor/CursorOverrideContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs @@ -12,7 +12,7 @@ namespace osu.Game.Graphics.Cursor /// /// A container which provides a which can be overridden by hovered s. /// - public class CursorOverrideContainer : Container, IProvideCursor + public class MenuCursorContainer : Container, IProvideCursor { protected override Container Content => content; private readonly Container content; @@ -25,7 +25,7 @@ namespace osu.Game.Graphics.Cursor public CursorContainer Cursor { get; } public bool ProvidingUserCursor => true; - public CursorOverrideContainer() + public MenuCursorContainer() { AddRangeInternal(new Drawable[] { diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs new file mode 100644 index 0000000000..be2412ccad --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Input; +using osu.Framework.Platform; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Graphics.UserInterface +{ + public class ExternalLinkButton : CompositeDrawable, IHasTooltip + { + public string Link { get; set; } + + private Color4 hoverColour; + private GameHost host; + + public ExternalLinkButton(string link = null) + { + Link = link; + Size = new Vector2(12); + InternalChild = new SpriteIcon + { + Icon = FontAwesome.fa_external_link, + RelativeSizeAxes = Axes.Both + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, GameHost host) + { + hoverColour = colours.Yellow; + this.host = host; + } + + protected override bool OnHover(InputState state) + { + InternalChild.FadeColour(hoverColour, 500, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + InternalChild.FadeColour(Color4.White, 500, Easing.OutQuint); + base.OnHoverLost(state); + } + + protected override bool OnClick(InputState state) + { + if(Link != null) + host.OpenUrlExternally(Link); + return true; + } + + public string TooltipText => "View in browser"; + } +} diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index fc14a9c6ba..d015a563f6 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -6,6 +6,7 @@ using System.Linq; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -157,7 +158,7 @@ namespace osu.Game.Graphics.UserInterface Margin = new MarginPadding { Top = 5, Bottom = 5 }, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - Text = (value as Enum)?.GetDescription() ?? value.ToString(), + Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetDescription() ?? value.ToString(), TextSize = 14, Font = @"Exo2.0-Bold", // Font should only turn bold when active? }, diff --git a/osu.Game/Graphics/UserInterface/SelectionState.cs b/osu.Game/Graphics/UserInterface/SelectionState.cs new file mode 100644 index 0000000000..079ae343eb --- /dev/null +++ b/osu.Game/Graphics/UserInterface/SelectionState.cs @@ -0,0 +1,11 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Graphics.UserInterface +{ + public enum SelectionState + { + NotSelected, + Selected + } +} diff --git a/osu.Game/IO/Archives/LegacyFilesystemReader.cs b/osu.Game/IO/Archives/LegacyFilesystemReader.cs index 7871f1439a..dcaaf41c77 100644 --- a/osu.Game/IO/Archives/LegacyFilesystemReader.cs +++ b/osu.Game/IO/Archives/LegacyFilesystemReader.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using osu.Framework.IO.File; namespace osu.Game.IO.Archives { @@ -15,19 +14,20 @@ namespace osu.Game.IO.Archives { private readonly string path; - public LegacyFilesystemReader(string path) : base(Path.GetFileName(path)) + public LegacyFilesystemReader(string path) + : base(Path.GetFileName(path)) { - this.path = path; + // re-get full path to standardise with Directory.GetFiles return values below. + this.path = Path.GetFullPath(path); } public override Stream GetStream(string name) => File.OpenRead(Path.Combine(path, name)); public override void Dispose() { - // no-op } - public override IEnumerable Filenames => Directory.GetFiles(path, "*", SearchOption.AllDirectories).Select(f => FileSafety.GetRelativePath(f, path)).ToArray(); + public override IEnumerable Filenames => Directory.GetFiles(path, "*", SearchOption.AllDirectories).Select(f => f.Replace(path, string.Empty).Trim(Path.DirectorySeparatorChar)).ToArray(); public override Stream GetUnderlyingStream() => null; } diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs new file mode 100644 index 0000000000..f28408bfb3 --- /dev/null +++ b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.Designer.cs @@ -0,0 +1,377 @@ +// +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("20180529055154_RemoveUniqueHashConstraints")] + partial class RemoveUniqueHashConstraints + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.3-rtm-10026"); + + 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("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.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + 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("RulesetID", "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.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + 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"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs new file mode 100644 index 0000000000..fe8c983a3f --- /dev/null +++ b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace osu.Game.Migrations +{ + public partial class RemoveUniqueHashConstraints : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo"); + + migrationBuilder.DropIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo"); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_Hash", + table: "BeatmapInfo", + column: "Hash", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_BeatmapInfo_MD5Hash", + table: "BeatmapInfo", + column: "MD5Hash", + unique: true); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index 2abbe7785f..d750d50b5b 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -1,7 +1,11 @@ // 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 { @@ -12,7 +16,7 @@ namespace osu.Game.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + .HasAnnotation("ProductVersion", "2.0.3-rtm-10026"); modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => { @@ -27,9 +31,9 @@ namespace osu.Game.Migrations b.Property("OverallDifficulty"); - b.Property("SliderMultiplier"); + b.Property("SliderMultiplier"); - b.Property("SliderTickRate"); + b.Property("SliderTickRate"); b.HasKey("ID"); @@ -91,11 +95,9 @@ namespace osu.Game.Migrations b.HasIndex("BeatmapSetInfoID"); - b.HasIndex("Hash") - .IsUnique(); + b.HasIndex("Hash"); - b.HasIndex("MD5Hash") - .IsUnique(); + b.HasIndex("MD5Hash"); b.HasIndex("MetadataID"); diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 9af142b9e8..adbedb2aac 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -14,16 +14,19 @@ namespace osu.Game.Online.API { protected override WebRequest CreateWebRequest() => new JsonWebRequest(Uri); + public T Result => ((JsonWebRequest)WebRequest).ResponseObject; + protected APIRequest() { base.Success += onSuccess; } - private void onSuccess() - { - Success?.Invoke(((JsonWebRequest)WebRequest).ResponseObject); - } + private void onSuccess() => Success?.Invoke(Result); + /// + /// Invoked on successful completion of an API request. + /// This will be scheduled to the API's internal scheduler (run on update thread automatically). + /// public new event APISuccessHandler Success; } @@ -52,7 +55,16 @@ namespace osu.Game.Online.API protected APIAccess API; protected WebRequest WebRequest; + /// + /// Invoked on successful completion of an API request. + /// This will be scheduled to the API's internal scheduler (run on update thread automatically). + /// public event APISuccessHandler Success; + + /// + /// Invoked on failure to complete an API request. + /// This will be scheduled to the API's internal scheduler (run on update thread automatically). + /// public event APIFailureHandler Failure; private bool cancelled; @@ -73,6 +85,7 @@ namespace osu.Game.Online.API throw new TimeoutException(@"API request timeout hit"); WebRequest = CreateWebRequest(); + WebRequest.Failed += Fail; WebRequest.AllowRetryOnTimeout = false; WebRequest.AddHeader("Authorization", $"Bearer {api.AccessToken}"); diff --git a/osu.Game/Online/API/Requests/APIResponseBeatmapSet.cs b/osu.Game/Online/API/Requests/APIResponseBeatmapSet.cs deleted file mode 100644 index 2661303652..0000000000 --- a/osu.Game/Online/API/Requests/APIResponseBeatmapSet.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using 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(@"status")] - private BeatmapSetOnlineStatus status { 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, - Status = status, - 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/GetBeatmapDetailsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapDetailsRequest.cs index b853da7e76..e3865be5fb 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapDetailsRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapDetailsRequest.cs @@ -1,46 +1,20 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using Newtonsoft.Json; using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetBeatmapDetailsRequest : APIRequest + public class GetBeatmapDetailsRequest : APIRequest { private readonly BeatmapInfo beatmap; - private string lookupString => beatmap.OnlineBeatmapID > 0 ? beatmap.OnlineBeatmapID.ToString() : $@"lookup?checksum={beatmap.Hash}&filename={System.Uri.EscapeUriString(beatmap.Path)}"; - public GetBeatmapDetailsRequest(BeatmapInfo beatmap) { this.beatmap = beatmap; } - protected override string Target => $@"beatmaps/{lookupString}"; - } - - public class GetBeatmapDetailsResponse : BeatmapMetrics - { - //the online API returns some metrics as a nested object. - [JsonProperty(@"failtimes")] - private BeatmapMetrics failTimes - { - set - { - Fails = value.Fails; - Retries = value.Retries; - } - } - - //and other metrics in the beatmap set. - [JsonProperty(@"beatmapset")] - private BeatmapMetrics beatmapSet - { - set - { - Ratings = value.Ratings; - } - } + protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}"; } } diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs new file mode 100644 index 0000000000..9d254ce29d --- /dev/null +++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetBeatmapRequest : APIRequest + { + private readonly BeatmapInfo beatmap; + + private string lookupString => beatmap.OnlineBeatmapID > 0 ? beatmap.OnlineBeatmapID.ToString() : $@"lookup?checksum={beatmap.MD5Hash}&filename={System.Uri.EscapeUriString(beatmap.Path)}"; + + public GetBeatmapRequest(BeatmapInfo beatmap) + { + this.beatmap = beatmap; + } + + protected override string Target => $@"beatmaps/{lookupString}"; + } +} diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs index 37dd77af46..d04e069cd6 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs @@ -1,9 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Online.API.Requests.Responses; + namespace osu.Game.Online.API.Requests { - public class GetBeatmapSetRequest : APIRequest + public class GetBeatmapSetRequest : APIRequest { private readonly int id; private readonly BeatmapSetLookupType type; diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs index cff6fd4ea5..3be5b91a0d 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -2,20 +2,15 @@ // 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.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; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetScoresRequest : APIRequest + public class GetScoresRequest : APIRequest { private readonly BeatmapInfo beatmap; private readonly LeaderboardScope scope; @@ -36,9 +31,9 @@ namespace osu.Game.Online.API.Requests Success += onSuccess; } - private void onSuccess(GetScoresResponse r) + private void onSuccess(APIScores r) { - foreach (OnlineScore score in r.Scores) + foreach (APIScore score in r.Scores) score.ApplyBeatmap(beatmap); } @@ -55,112 +50,4 @@ namespace osu.Game.Online.API.Requests protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores"; } - - public class GetScoresResponse - { - [JsonProperty(@"scores")] - public IEnumerable Scores; - } - - public class OnlineScore : Score - { - [JsonProperty(@"score")] - private double totalScore - { - set { TotalScore = value; } - } - - [JsonProperty(@"max_combo")] - private int maxCombo - { - set { MaxCombo = value; } - } - - [JsonProperty(@"user")] - private User user - { - set { User = value; } - } - - [JsonProperty(@"replay_data")] - private Replay replay - { - set { Replay = value; } - } - - [JsonProperty(@"mode_int")] - public int OnlineRulesetID { get; set; } - - [JsonProperty(@"score_id")] - private long onlineScoreID - { - set { OnlineScoreID = value; } - } - - [JsonProperty(@"created_at")] - private DateTimeOffset date - { - 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 - { - set - { - foreach (var kvp in value) - { - HitResult newKey; - switch (kvp.Key) - { - case @"count_300": - newKey = HitResult.Great; - break; - case @"count_100": - newKey = HitResult.Good; - break; - case @"count_50": - newKey = HitResult.Meh; - break; - case @"count_miss": - newKey = HitResult.Miss; - break; - default: - continue; - } - - Statistics.Add(newKey, kvp.Value); - } - } - } - - [JsonProperty(@"mods")] - private string[] modStrings { get; set; } - - public void ApplyBeatmap(BeatmapInfo beatmap) - { - Beatmap = beatmap; - 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 index 48e9babd97..b847a954f6 100644 --- a/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserBeatmapsRequest.cs @@ -3,10 +3,11 @@ using Humanizer; using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetUserBeatmapsRequest : APIRequest> + public class GetUserBeatmapsRequest : APIRequest> { private readonly long userId; private readonly int offset; diff --git a/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs index 106f3d4fb1..9a54aafa82 100644 --- a/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserMostPlayedBeatmapsRequest.cs @@ -1,14 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using Newtonsoft.Json; -using osu.Game.Beatmaps; -using osu.Game.Rulesets; using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetUserMostPlayedBeatmapsRequest : APIRequest> + public class GetUserMostPlayedBeatmapsRequest : APIRequest> { private readonly long userId; private readonly int offset; @@ -21,28 +19,4 @@ namespace osu.Game.Online.API.Requests 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/GetUserRecentActivitiesRequest.cs b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs index ded0e4f80d..e1cad1a532 100644 --- a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs @@ -1,15 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using Newtonsoft.Json; -using osu.Game.Rulesets.Scoring; -using Humanizer; -using System; using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetUserRecentActivitiesRequest : APIRequest> + public class GetUserRecentActivitiesRequest : APIRequest> { private readonly long userId; private readonly int offset; @@ -23,86 +20,6 @@ namespace osu.Game.Online.API.Requests protected override string Target => $"users/{userId}/recent_activity?offset={offset}"; } - public class RecentActivity - { - [JsonProperty("id")] - public int ID; - - [JsonProperty("createdAt")] - public DateTimeOffset CreatedAt; - - [JsonProperty] - private string type - { - set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize()); - } - - public RecentActivityType Type; - - [JsonProperty] - private string scoreRank - { - set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value); - } - - public ScoreRank ScoreRank; - - [JsonProperty("rank")] - public int Rank; - - [JsonProperty("approval")] - public BeatmapApproval Approval; - - [JsonProperty("count")] - public int Count; - - [JsonProperty("mode")] - public string Mode; - - [JsonProperty("beatmap")] - public RecentActivityBeatmap Beatmap; - - [JsonProperty("beatmapset")] - public RecentActivityBeatmap Beatmapset; - - [JsonProperty("user")] - public RecentActivityUser User; - - [JsonProperty("achievement")] - public RecentActivityAchievement Achievement; - - public class RecentActivityBeatmap - { - [JsonProperty("title")] - public string Title; - - [JsonProperty("url")] - public string Url; - } - - public class RecentActivityUser - { - [JsonProperty("username")] - public string Username; - - [JsonProperty("url")] - public string Url; - - [JsonProperty("previousUsername")] - public string PreviousUsername; - } - - public class RecentActivityAchievement - { - [JsonProperty("slug")] - public string Slug; - - [JsonProperty("name")] - public string Name; - } - - } - public enum RecentActivityType { Achievement, diff --git a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs index 7757f529e0..ea14135a61 100644 --- a/osu.Game/Online/API/Requests/GetUserScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserScoresRequest.cs @@ -2,10 +2,11 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetUserScoresRequest : APIRequest> + public class GetUserScoresRequest : APIRequest> { private readonly long userId; private readonly ScoreType type; diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs index 575d0464aa..17dd9263c9 100644 --- a/osu.Game/Online/API/Requests/GetUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs @@ -2,19 +2,12 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using Newtonsoft.Json; -using osu.Game.Users; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.API.Requests { - public class GetUsersRequest : APIRequest> + public class GetUsersRequest : APIRequest> { protected override string Target => @"rankings/osu/performance"; } - - public class RankingEntry - { - [JsonProperty] - public User User; - } } diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs new file mode 100644 index 0000000000..99e4392374 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -0,0 +1,85 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIBeatmap : BeatmapMetadata + { + [JsonProperty(@"id")] + public int OnlineBeatmapID { get; set; } + + [JsonProperty(@"beatmapset_id")] + public int OnlineBeatmapSetID { 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, + BeatmapSet = new BeatmapSetInfo + { + OnlineBeatmapSetID = OnlineBeatmapSetID, + }, + 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/Responses/APIBeatmapMetrics.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapMetrics.cs new file mode 100644 index 0000000000..e67c60cb91 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapMetrics.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Beatmaps; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIBeatmapMetrics : BeatmapMetrics + { + //the online API returns some metrics as a nested object. + [JsonProperty(@"failtimes")] + private BeatmapMetrics failTimes + { + set + { + Fails = value.Fails; + Retries = value.Retries; + } + } + + //and other metrics in the beatmap set. + [JsonProperty(@"beatmapset")] + private BeatmapMetrics beatmapSet + { + set => Ratings = value.Ratings; + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs new file mode 100644 index 0000000000..3b6bb565b0 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIBeatmapSet : BeatmapMetadata // todo: this is a bit wrong... + { + [JsonProperty(@"covers")] + private BeatmapSetOnlineCovers covers { get; set; } + + private int? onlineBeatmapSetID; + + [JsonProperty(@"id")] + public int? OnlineBeatmapSetID + { + get { return onlineBeatmapSetID; } + set { onlineBeatmapSetID = value > 0 ? value : null; } + } + + [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(@"storyboard")] + private bool hasStoryboard { get; set; } + + [JsonProperty(@"status")] + private BeatmapSetOnlineStatus status { 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, + Status = status, + HasVideo = hasVideo, + HasStoryboard = hasStoryboard, + Submitted = submitted, + Ranked = ranked, + LastUpdated = lastUpdated, + }, + Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(), + }; + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs new file mode 100644 index 0000000000..2c65a37cf8 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIRecentActivity.cs @@ -0,0 +1,89 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using Humanizer; +using Newtonsoft.Json; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIRecentActivity + { + [JsonProperty("id")] + public int ID; + + [JsonProperty("createdAt")] + public DateTimeOffset CreatedAt; + + [JsonProperty] + private string type + { + set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize()); + } + + public RecentActivityType Type; + + [JsonProperty] + private string scoreRank + { + set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value); + } + + public ScoreRank ScoreRank; + + [JsonProperty("rank")] + public int Rank; + + [JsonProperty("approval")] + public BeatmapApproval Approval; + + [JsonProperty("count")] + public int Count; + + [JsonProperty("mode")] + public string Mode; + + [JsonProperty("beatmap")] + public RecentActivityBeatmap Beatmap; + + [JsonProperty("beatmapset")] + public RecentActivityBeatmap Beatmapset; + + [JsonProperty("user")] + public RecentActivityUser User; + + [JsonProperty("achievement")] + public RecentActivityAchievement Achievement; + + public class RecentActivityBeatmap + { + [JsonProperty("title")] + public string Title; + + [JsonProperty("url")] + public string Url; + } + + public class RecentActivityUser + { + [JsonProperty("username")] + public string Username; + + [JsonProperty("url")] + public string Url; + + [JsonProperty("previousUsername")] + public string PreviousUsername; + } + + public class RecentActivityAchievement + { + [JsonProperty("slug")] + public string Slug; + + [JsonProperty("name")] + public string Name; + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIScore.cs b/osu.Game/Online/API/Requests/Responses/APIScore.cs new file mode 100644 index 0000000000..a398bf46ee --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIScore.cs @@ -0,0 +1,117 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Users; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIScore : Score + { + [JsonProperty(@"score")] + private double totalScore + { + set => TotalScore = value; + } + + [JsonProperty(@"max_combo")] + private int maxCombo + { + set => MaxCombo = value; + } + + [JsonProperty(@"user")] + private User user + { + set => User = value; + } + + [JsonProperty(@"replay_data")] + private Replay replay + { + set => Replay = value; + } + + [JsonProperty(@"mode_int")] + public int OnlineRulesetID { get; set; } + + [JsonProperty(@"score_id")] + private long onlineScoreID + { + set => OnlineScoreID = value; + } + + [JsonProperty(@"created_at")] + private DateTimeOffset date + { + 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 + { + set + { + foreach (var kvp in value) + { + HitResult newKey; + switch (kvp.Key) + { + case @"count_300": + newKey = HitResult.Great; + break; + case @"count_100": + newKey = HitResult.Good; + break; + case @"count_50": + newKey = HitResult.Meh; + break; + case @"count_miss": + newKey = HitResult.Miss; + break; + default: + continue; + } + + Statistics.Add(newKey, kvp.Value); + } + } + } + + [JsonProperty(@"mods")] + private string[] modStrings { get; set; } + + public void ApplyBeatmap(BeatmapInfo beatmap) + { + Beatmap = beatmap; + 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/Responses/APIScores.cs b/osu.Game/Online/API/Requests/Responses/APIScores.cs new file mode 100644 index 0000000000..b4213db253 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIScores.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIScores + { + [JsonProperty(@"scores")] + public IEnumerable Scores; + } +} diff --git a/osu.Desktop.Deploy/GitHubObject.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs similarity index 52% rename from osu.Desktop.Deploy/GitHubObject.cs rename to osu.Game/Online/API/Requests/Responses/APIUser.cs index 02ff16fa69..54c8451456 100644 --- a/osu.Desktop.Deploy/GitHubObject.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs @@ -2,15 +2,13 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using Newtonsoft.Json; +using osu.Game.Users; -namespace osu.Desktop.Deploy +namespace osu.Game.Online.API.Requests.Responses { - public class GitHubObject + public class APIUser { - [JsonProperty(@"id")] - public int Id; - - [JsonProperty(@"name")] - public string Name; + [JsonProperty] + public User User; } } diff --git a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs new file mode 100644 index 0000000000..8a5aea9e97 --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APIUserMostPlayedBeatmap + { + [JsonProperty("beatmap_id")] + public int BeatmapID; + + [JsonProperty("count")] + public int PlayCount; + + [JsonProperty] + private BeatmapInfo beatmap; + + [JsonProperty] + private APIBeatmapSet beatmapSet; + + public BeatmapInfo GetBeatmapInfo(RulesetStore rulesets) + { + BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets); + beatmap.BeatmapSet = setInfo; + beatmap.Metadata = setInfo.Metadata; + return beatmap; + } + } +} diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index d68314f8fc..2a154d1230 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -3,13 +3,14 @@ using System.Collections.Generic; using System.ComponentModel; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Direct; using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { - public class SearchBeatmapSetsRequest : APIRequest> + public class SearchBeatmapSetsRequest : APIRequest> { private readonly string query; private readonly RulesetInfo ruleset; diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index f1c23e9e84..b076afbcdb 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using osu.Framework.Configuration; using osu.Game.Beatmaps; using osu.Game.Users; @@ -12,9 +13,10 @@ namespace osu.Game.Online.Multiplayer public Bindable Name = new Bindable(); public Bindable Host = new Bindable(); public Bindable Status = new Bindable(); + public Bindable Availability = new Bindable(); public Bindable Type = new Bindable(); public Bindable Beatmap = new Bindable(); public Bindable MaxParticipants = new Bindable(); - public Bindable Participants = new Bindable(); + public Bindable> Participants = new Bindable>(); } } diff --git a/osu.Game/Online/Multiplayer/RoomAvailability.cs b/osu.Game/Online/Multiplayer/RoomAvailability.cs new file mode 100644 index 0000000000..6c154207ff --- /dev/null +++ b/osu.Game/Online/Multiplayer/RoomAvailability.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; + +namespace osu.Game.Online.Multiplayer +{ + public enum RoomAvailability + { + Public, + + [Description(@"Friends Only")] + FriendsOnly, + + [Description(@"Invite Only")] + InviteOnly, + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index df79a20f53..a9d03effd7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -77,7 +77,7 @@ namespace osu.Game public float ToolbarOffset => Toolbar.Position.Y + Toolbar.DrawHeight; - public readonly BindableBool ShowOverlays = new BindableBool(); + public readonly Bindable OverlayActivationMode = new Bindable(); private OsuScreen screenStack; @@ -93,6 +93,8 @@ namespace osu.Game private SettingsOverlay settings; + private readonly List overlays = new List(); + // todo: move this to SongSelect once Screen has the ability to unsuspend. public readonly Bindable> SelectedMods = new Bindable>(new List()); @@ -105,6 +107,17 @@ namespace osu.Game public void ToggleDirect() => direct.ToggleVisibility(); + /// + /// Close all game-wide overlays. + /// + /// Whether the toolbar should also be hidden. + public void CloseAllOverlays(bool toolbar = true) + { + foreach (var o in overlays) + o.State = Visibility.Hidden; + if (toolbar) Toolbar.State = Visibility.Hidden; + } + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => @@ -211,7 +224,7 @@ namespace osu.Game protected override void LoadComplete() { - // this needs to be cached before base.LoadComplete as it is used by CursorOverrideContainer. + // this needs to be cached before base.LoadComplete as it is used by MenuCursorContainer. dependencies.Cache(screenshotManager = new ScreenshotManager()); base.LoadComplete(); @@ -219,7 +232,7 @@ namespace osu.Game // The next time this is updated is in UpdateAfterChildren, which occurs too late and results // in the cursor being shown for a few frames during the intro. // This prevents the cursor from showing until we have a screen with CursorVisible = true - CursorOverrideContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; + MenuCursorContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; // hook up notifications to components. SkinManager.PostNotification = n => notifications?.Post(n); @@ -250,7 +263,7 @@ namespace osu.Game Depth = -5, OnHome = delegate { - hideAllOverlays(); + CloseAllOverlays(false); intro?.ChildScreen?.MakeCurrent(); }, }, overlayContent.Add); @@ -307,6 +320,8 @@ namespace osu.Game // ensure only one of these overlays are open at once. var singleDisplayOverlays = new OverlayContainer[] { chat, social, direct }; + overlays.AddRange(singleDisplayOverlays); + foreach (var overlay in singleDisplayOverlays) { overlay.StateChanged += state => @@ -322,6 +337,8 @@ namespace osu.Game } var singleDisplaySideOverlays = new OverlayContainer[] { settings, notifications }; + overlays.AddRange(singleDisplaySideOverlays); + foreach (var overlay in singleDisplaySideOverlays) { overlay.StateChanged += state => @@ -338,6 +355,8 @@ namespace osu.Game // 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 }; + overlays.AddRange(informationalOverlays); + foreach (var overlay in informationalOverlays) { overlay.StateChanged += state => @@ -352,6 +371,11 @@ namespace osu.Game }; } + OverlayActivationMode.ValueChanged += v => + { + if (v != OverlayActivation.All) CloseAllOverlays(); + }; + void updateScreenOffset() { float offset = 0; @@ -366,21 +390,6 @@ namespace osu.Game settings.StateChanged += _ => updateScreenOffset(); notifications.StateChanged += _ => updateScreenOffset(); - - notifications.Enabled.BindTo(ShowOverlays); - - ShowOverlays.ValueChanged += show => - { - //central game screen change logic. - if (!show) - { - hideAllOverlays(); - musicController.State = Visibility.Hidden; - Toolbar.State = Visibility.Hidden; - } - else - Toolbar.State = Visibility.Visible; - }; } private void forwardLoggedErrorsToNotifications() @@ -497,16 +506,6 @@ namespace osu.Game private OsuScreen currentScreen; private FrameworkConfigManager frameworkConfig; - private void hideAllOverlays() - { - settings.State = Visibility.Hidden; - chat.State = Visibility.Hidden; - direct.State = Visibility.Hidden; - social.State = Visibility.Hidden; - userProfile.State = Visibility.Hidden; - notifications.State = Visibility.Hidden; - } - protected override bool OnExiting() { if (screenStack.ChildScreen == null) return false; @@ -547,7 +546,7 @@ namespace osu.Game mainContent.Padding = new MarginPadding { Top = ToolbarOffset }; - CursorOverrideContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; + MenuCursorContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; } private void screenAdded(Screen newScreen) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 487cb50c9a..a5779a2293 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -3,14 +3,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Configuration; -using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.IO.Stores; @@ -31,6 +29,7 @@ using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; +using DebugUtils = osu.Game.Utils.DebugUtils; namespace osu.Game { @@ -57,15 +56,16 @@ namespace osu.Game protected SettingsStore SettingsStore; - protected CursorOverrideContainer CursorOverrideContainer; + protected RulesetConfigCache RulesetConfigCache; - protected override string MainResourceFile => @"osu.Game.Resources.dll"; + protected MenuCursorContainer MenuCursorContainer; private Container content; protected override Container Content => content; - public Bindable Beatmap { get; private set; } + private OsuBindableBeatmap beatmap; + protected BindableBeatmap Beatmap => beatmap; private Bindable fpsDisplayVisible; @@ -100,6 +100,8 @@ namespace osu.Game [BackgroundDependencyLoader] private void load() { + Resources.AddStore(new DllResourceStore(@"osu.Game.Resources.dll")); + dependencies.Cache(contextFactory = new DatabaseContextFactory(Host)); dependencies.Cache(new LargeTextureStore(new RawTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")))); @@ -123,6 +125,7 @@ namespace osu.Game dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory, Host, BeatmapManager, RulesetStore)); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); + dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(new OsuColour()); fileImporters.Add(BeatmapManager); @@ -157,33 +160,15 @@ namespace osu.Game Fonts.AddStore(new GlyphStore(Resources, @"Fonts/Venera-Light")); var defaultBeatmap = new DummyWorkingBeatmap(this); - Beatmap = new NonNullableBindable(defaultBeatmap); + beatmap = new OsuBindableBeatmap(defaultBeatmap, Audio); 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; - - // compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) - if (!trackLoaded || lastBeatmap?.Track != b.Track) - { - if (trackLoaded) - { - Debug.Assert(lastBeatmap != null); - Debug.Assert(lastBeatmap.Track != null); - - lastBeatmap.RecycleTrack(); - } - - Audio.Track.AddItem(b.Track); - } - - lastBeatmap = b; - }; + dependencies.CacheAs(beatmap); + dependencies.CacheAs(beatmap); FileStore.Cleanup(); @@ -191,42 +176,19 @@ namespace osu.Game GlobalActionContainer globalBinding; - CursorOverrideContainer = new CursorOverrideContainer { RelativeSizeAxes = Axes.Both }; - CursorOverrideContainer.Child = globalBinding = new GlobalActionContainer(this) + MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }; + MenuCursorContainer.Child = globalBinding = new GlobalActionContainer(this) { RelativeSizeAxes = Axes.Both, - Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both } + Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both } }; - base.Content.Add(new DrawSizePreservingFillContainer { Child = CursorOverrideContainer }); + base.Content.Add(new DrawSizePreservingFillContainer { Child = MenuCursorContainer }); KeyBindingStore.Register(globalBinding); dependencies.Cache(globalBinding); } - private void runMigrations() - { - try - { - using (var db = contextFactory.GetForWrite()) - db.Context.Migrate(); - } - catch (MigrationFailedException e) - { - Logger.Error(e.InnerException ?? e, "Migration failed! We'll be starting with a fresh database.", LoggingTarget.Database); - - // 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 db = contextFactory.GetForWrite()) - db.Context.Migrate(); - } - } - - private WorkingBeatmap lastBeatmap; - protected override void LoadComplete() { base.LoadComplete(); @@ -238,6 +200,29 @@ namespace osu.Game fpsDisplayVisible.TriggerChange(); } + private void runMigrations() + { + try + { + using (var db = contextFactory.GetForWrite(false)) + db.Context.Migrate(); + } + catch (Exception e) + { + Logger.Error(e.InnerException ?? e, "Migration failed! We'll be starting with a fresh database.", LoggingTarget.Database); + + // 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); + + // only run once more, then hard bail. + using (var db = contextFactory.GetForWrite(false)) + db.Context.Migrate(); + } + } + public override void SetHost(GameHost host) { if (LocalConfig == null) @@ -256,5 +241,26 @@ namespace osu.Game } public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray(); + + private class OsuBindableBeatmap : BindableBeatmap + { + public OsuBindableBeatmap(WorkingBeatmap defaultValue, AudioManager audioManager) + : this(defaultValue) + { + RegisterAudioManager(audioManager); + } + + private OsuBindableBeatmap(WorkingBeatmap defaultValue) + : base(defaultValue) + { + } + + public override BindableBeatmap GetBoundCopy() + { + var copy = new OsuBindableBeatmap(Default); + copy.BindTo(this); + return copy; + } + } } } diff --git a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs index 66e3148065..398518ef2b 100644 --- a/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs +++ b/osu.Game/Overlays/BeatmapSet/AuthorInfo.cs @@ -49,8 +49,8 @@ namespace osu.Game.Overlays.BeatmapSet fields.Children = new Drawable[] { - new Field("made by", BeatmapSet.Metadata.Author.Username, @"Exo2.0-RegularItalic"), - new Field("submitted on", online.Submitted.ToString(@"MMM d, yyyy"), @"Exo2.0-Bold") + new Field("mapped by", BeatmapSet.Metadata.Author.Username, @"Exo2.0-RegularItalic"), + new Field("submitted on", online.Submitted.ToString(@"MMMM d, yyyy"), @"Exo2.0-Bold") { Margin = new MarginPadding { Top = 5 }, }, @@ -58,11 +58,11 @@ namespace osu.Game.Overlays.BeatmapSet if (online.Ranked.HasValue) { - fields.Add(new Field("ranked on ", online.Ranked.Value.ToString(@"MMM d, yyyy"), @"Exo2.0-Bold")); + fields.Add(new Field("ranked on", online.Ranked.Value.ToString(@"MMMM d, yyyy"), @"Exo2.0-Bold")); } else if (online.LastUpdated.HasValue) { - fields.Add(new Field("last updated on ", online.LastUpdated.Value.ToString(@"MMM d, yyyy"), @"Exo2.0-Bold")); + fields.Add(new Field("last updated on", online.LastUpdated.Value.ToString(@"MMMM d, yyyy"), @"Exo2.0-Bold")); } } diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs index c699ae2328..4fce6a49fb 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs @@ -3,6 +3,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using OpenTK; @@ -11,10 +13,11 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons { public class DownloadButton : HeaderButton { - public DownloadButton(string title, string subtitle) + public DownloadButton(BeatmapSetInfo set, bool noVideo = false) { Width = 120; + BeatmapSetDownloader downloader; Add(new Container { Depth = -1, @@ -22,6 +25,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons Padding = new MarginPadding { Horizontal = 10 }, Children = new Drawable[] { + downloader = new BeatmapSetDownloader(set, noVideo), new FillFlowContainer { Anchor = Anchor.CentreLeft, @@ -32,13 +36,13 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons { new OsuSpriteText { - Text = title, + Text = "Download", TextSize = 13, Font = @"Exo2.0-Bold", }, new OsuSpriteText { - Text = subtitle, + Text = set.OnlineInfo.HasVideo && noVideo ? "without Video" : string.Empty, TextSize = 11, Font = @"Exo2.0-Bold", }, @@ -54,6 +58,25 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons }, }, }); + + Action = () => + { + if (!downloader.Download()) + { + Content.MoveToX(-5, 50, Easing.OutSine).Then() + .MoveToX(5, 100, Easing.InOutSine).Then() + .MoveToX(-5, 100, Easing.InOutSine).Then() + .MoveToX(0, 50, Easing.InSine); + } + }; + + downloader.Downloaded.ValueChanged += d => + { + if (d) + this.FadeOut(200); + else + this.FadeIn(200); + }; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 9b25d61f58..afba99f928 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.BeatmapSet.Buttons; using OpenTK; using OpenTK.Graphics; @@ -25,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapSet private const float buttons_spacing = 5; private readonly Box tabsBg; - private readonly Container coverContainer; + private readonly UpdateableBeatmapSetCover cover; private readonly OsuSpriteText title, artist; private readonly Container noVideoButtons; private readonly FillFlowContainer videoButtons; @@ -34,9 +35,6 @@ namespace osu.Game.Overlays.BeatmapSet private readonly BeatmapSetOnlineStatusPill onlineStatusPill; public Details Details; - private BeatmapManager beatmaps; - private DelayedLoadWrapper cover; - public readonly BeatmapPicker Picker; private BeatmapSetInfo beatmapSet; @@ -61,28 +59,31 @@ namespace osu.Game.Overlays.BeatmapSet title.Text = BeatmapSet?.Metadata.Title ?? string.Empty; artist.Text = BeatmapSet?.Metadata.Artist ?? string.Empty; onlineStatusPill.Status = BeatmapSet?.OnlineInfo.Status ?? BeatmapSetOnlineStatus.None; + cover.BeatmapSet = BeatmapSet; - cover?.FadeOut(400, Easing.Out); if (BeatmapSet != null) { downloadButtonsContainer.FadeIn(transition_duration); favouriteButton.FadeIn(transition_duration); - noVideoButtons.FadeTo(BeatmapSet.OnlineInfo.HasVideo ? 0 : 1, transition_duration); - videoButtons.FadeTo(BeatmapSet.OnlineInfo.HasVideo ? 1 : 0, transition_duration); - - coverContainer.Add(cover = new DelayedLoadWrapper( - new BeatmapSetCover(BeatmapSet) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fill, - OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), - }, 300) + if (BeatmapSet.OnlineInfo.HasVideo) { - RelativeSizeAxes = Axes.Both, - }); + videoButtons.Children = new[] + { + new DownloadButton(BeatmapSet), + new DownloadButton(BeatmapSet, true), + }; + + videoButtons.FadeIn(transition_duration); + noVideoButtons.FadeOut(transition_duration); + } + else + { + noVideoButtons.Child = new DownloadButton(BeatmapSet); + + noVideoButtons.FadeIn(transition_duration); + videoButtons.FadeOut(transition_duration); + } } else { @@ -93,6 +94,7 @@ namespace osu.Game.Overlays.BeatmapSet public Header() { + ExternalLinkButton externalLink; RelativeSizeAxes = Axes.X; Height = 400; Masking = true; @@ -128,12 +130,7 @@ namespace osu.Game.Overlays.BeatmapSet RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - coverContainer = new Container + cover = new UpdateableBeatmapSetCover { RelativeSizeAxes = Axes.Both, }, @@ -160,10 +157,24 @@ namespace osu.Game.Overlays.BeatmapSet Height = 113, Child = Picker = new BeatmapPicker(), }, - title = new OsuSpriteText + new FillFlowContainer { - Font = @"Exo2.0-BoldItalic", - TextSize = 37, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + title = new OsuSpriteText + { + Font = @"Exo2.0-BoldItalic", + TextSize = 37, + }, + externalLink = new ExternalLinkButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Left = 3, Bottom = 4 }, //To better lineup with the font + }, + } }, artist = new OsuSpriteText { @@ -195,27 +206,12 @@ namespace osu.Game.Overlays.BeatmapSet { RelativeSizeAxes = Axes.Both, Alpha = 0f, - Child = new DownloadButton("Download", @"") - { - Action = () => download(false), - }, }, videoButtons = new FillFlowContainer { RelativeSizeAxes = Axes.Both, Spacing = new Vector2(buttons_spacing), Alpha = 0f, - Children = new[] - { - new DownloadButton("Download", "with Video") - { - Action = () => download(false), - }, - new DownloadButton("Download", "without Video") - { - Action = () => download(true), - }, - }, }, }, }, @@ -247,44 +243,14 @@ namespace osu.Game.Overlays.BeatmapSet }; Picker.Beatmap.ValueChanged += b => Details.Beatmap = b; + Picker.Beatmap.ValueChanged += b => externalLink.Link = $@"https://osu.ppy.sh/beatmapsets/{BeatmapSet?.OnlineBeatmapSetID}#{b?.Ruleset.ShortName}/{b?.OnlineBeatmapID}"; } [BackgroundDependencyLoader] - private void load(OsuColour colours, BeatmapManager beatmaps) + private void load(OsuColour colours) { tabsBg.Colour = colours.Gray3; - this.beatmaps = beatmaps; - - beatmaps.ItemAdded += handleBeatmapAdd; - updateDisplay(); } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - if (beatmaps != null) beatmaps.ItemAdded -= handleBeatmapAdd; - } - - private void handleBeatmapAdd(BeatmapSetInfo beatmap) => Schedule(() => - { - 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/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index cd0b7386e8..53216ad666 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.BeatmapSet private const float metadata_width = 225; private const float spacing = 20; - private readonly MetadataSection description, source, tags; + private readonly MetadataSection source, tags; private readonly Box successRateBackground; private readonly SuccessRate successRate; @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.BeatmapSet Child = new Container { RelativeSizeAxes = Axes.Both, - Child = description = new MetadataSection("Description"), + Child = new MetadataSection("Description"), }, }, new Container @@ -135,8 +135,6 @@ namespace osu.Game.Overlays.BeatmapSet private void load(OsuColour colours) { successRateBackground.Colour = colours.GrayE; - source.TextColour = description.TextColour = colours.Gray5; - tags.TextColour = colours.BlueDark; updateDisplay(); } @@ -195,7 +193,7 @@ namespace osu.Game.Overlays.BeatmapSet [BackgroundDependencyLoader] private void load(OsuColour colours) { - header.Colour = colours.Gray5; + header.Colour = textFlow.Colour = colours.Gray5; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/DrawableScore.cs b/osu.Game/Overlays/BeatmapSet/Scores/DrawableScore.cs index 10e689698d..fc8b3a6800 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/DrawableScore.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/DrawableScore.cs @@ -9,7 +9,7 @@ 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.Online.API.Requests.Responses; using osu.Game.Overlays.Profile.Sections.Ranks; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly Box background; - public DrawableScore(int index, OnlineScore score) + public DrawableScore(int index, APIScore score) { ScoreModsContainer modsContainer; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/DrawableTopScore.cs b/osu.Game/Overlays/BeatmapSet/Scores/DrawableTopScore.cs index 8ac79eabb4..b3ceffd35e 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/DrawableTopScore.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/DrawableTopScore.cs @@ -11,7 +11,7 @@ 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.Online.API.Requests.Responses; using osu.Game.Overlays.Profile.Sections.Ranks; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -42,8 +42,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly InfoColumn statistics; private readonly ScoreModsContainer modsContainer; - private OnlineScore score; - public OnlineScore Score + private APIScore score; + public APIScore Score { get { return score; } set diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 185282bec9..626de14c98 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -11,6 +11,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.BeatmapSet.Scores { @@ -28,10 +29,10 @@ namespace osu.Game.Overlays.BeatmapSet.Scores set => loadingAnimation.FadeTo(value ? 1 : 0, fade_duration); } - private IEnumerable scores; + private IEnumerable scores; private BeatmapInfo beatmap; - public IEnumerable Scores + public IEnumerable Scores { get { return scores; } set diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/Direct/DirectGridPanel.cs index 9715615d14..e286837746 100644 --- a/osu.Game/Overlays/Direct/DirectGridPanel.cs +++ b/osu.Game/Overlays/Direct/DirectGridPanel.cs @@ -149,7 +149,7 @@ namespace osu.Game.Overlays.Direct { new OsuSpriteText { - Text = $"from {SetInfo.Metadata.Source}", + Text = SetInfo.Metadata.Source, TextSize = 14, Shadow = false, Colour = colours.Gray5, @@ -166,14 +166,13 @@ namespace osu.Game.Overlays.Direct }, }, }, - new DownloadButton + new DownloadButton(SetInfo) { Size = new Vector2(30), Margin = new MarginPadding(horizontal_padding), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Colour = colours.Gray5, - Action = StartDownload }, }, }, @@ -195,18 +194,18 @@ 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, - }, statusContainer = new FillFlowContainer { AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Top = 5, Left = 5 }, Spacing = new Vector2(5), }, + playButton = new PlayButton(SetInfo) + { + Margin = new MarginPadding { Top = 5, Left = 10 }, + Size = new Vector2(30), + Alpha = 0, + }, }); if (SetInfo.OnlineInfo?.HasVideo ?? false) @@ -214,24 +213,31 @@ namespace osu.Game.Overlays.Direct statusContainer.Add(new IconPill(FontAwesome.fa_film)); } + if (SetInfo.OnlineInfo?.HasStoryboard ?? false) + { + statusContainer.Add(new IconPill(FontAwesome.fa_image)); + } + statusContainer.Add(new BeatmapSetOnlineStatusPill(12, new MarginPadding { Horizontal = 10, Vertical = 5 }) { Status = SetInfo.OnlineInfo?.Status ?? BeatmapSetOnlineStatus.None, }); + + PreviewPlaying.ValueChanged += _ => updateStatusContainer(); } protected override bool OnHover(InputState state) { - statusContainer.FadeOut(120, Easing.InOutQuint); - + updateStatusContainer(); return base.OnHover(state); } protected override void OnHoverLost(InputState state) { base.OnHoverLost(state); - - statusContainer.FadeIn(120, Easing.InOutQuint); + updateStatusContainer(); } + + private void updateStatusContainer() => statusContainer.FadeTo(IsHovered || PreviewPlaying ? 0 : 1, 120, Easing.InOutQuint); } } diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index 13398a4a32..812a0e2073 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -12,28 +12,31 @@ using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Game.Beatmaps; namespace osu.Game.Overlays.Direct { public class DirectListPanel : DirectPanel { + private const float transition_duration = 120; private const float horizontal_padding = 10; private const float vertical_padding = 5; private const float height = 70; + private PlayButton playButton; + private Box progressBar; + private Container downloadContainer; + + protected override PlayButton PlayButton => playButton; + protected override Box PreviewBar => progressBar; + public DirectListPanel(BeatmapSetInfo beatmap) : base(beatmap) { RelativeSizeAxes = Axes.X; Height = height; } - private PlayButton playButton; - private Box progressBar; - - protected override PlayButton PlayButton => playButton; - protected override Box PreviewBar => progressBar; - [BackgroundDependencyLoader] private void load(LocalisationEngine localisation, OsuColour colours) { @@ -59,7 +62,7 @@ namespace osu.Game.Overlays.Direct AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, LayoutEasing = Easing.OutQuint, - LayoutDuration = 120, + LayoutDuration = transition_duration, Spacing = new Vector2(10, 0), Children = new Drawable[] { @@ -104,53 +107,69 @@ namespace osu.Game.Overlays.Direct Anchor = Anchor.TopRight, Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Margin = new MarginPadding { Right = height - vertical_padding * 2 + vertical_padding }, + Direction = FillDirection.Horizontal, + LayoutEasing = Easing.OutQuint, + LayoutDuration = transition_duration, Children = new Drawable[] { - new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0) + downloadContainer = new Container { - Margin = new MarginPadding { Right = 1 }, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Alpha = 0, + Child = new DownloadButton(SetInfo) + { + Size = new Vector2(height - vertical_padding * 2), + Margin = new MarginPadding { Left = vertical_padding }, + }, }, - new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0), new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new[] + Direction = FillDirection.Vertical, + Children = new Drawable[] { - new OsuSpriteText + new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0) { - Text = "mapped by ", - TextSize = 14, + Margin = new MarginPadding { Right = 1 }, + }, + new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0), + new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + new OsuSpriteText + { + Text = "mapped by ", + TextSize = 14, + }, + new OsuSpriteText + { + Text = SetInfo.Metadata.Author.Username, + TextSize = 14, + Font = @"Exo2.0-SemiBoldItalic", + }, + }, }, new OsuSpriteText { - Text = SetInfo.Metadata.Author.Username, + Text = SetInfo.Metadata.Source, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, TextSize = 14, - Font = @"Exo2.0-SemiBoldItalic", + Alpha = string.IsNullOrEmpty(SetInfo.Metadata.Source) ? 0f : 1f, }, }, }, - new OsuSpriteText - { - Text = $"from {SetInfo.Metadata.Source}", - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - TextSize = 14, - Alpha = string.IsNullOrEmpty(SetInfo.Metadata.Source) ? 0f : 1f, - }, }, }, - new DownloadButton - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Size = new Vector2(height - vertical_padding * 2), - Action = StartDownload - }, }, }, progressBar = new Box @@ -165,5 +184,17 @@ namespace osu.Game.Overlays.Direct }, }); } + + protected override bool OnHover(InputState state) + { + downloadContainer.FadeIn(transition_duration, Easing.InOutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + downloadContainer.FadeOut(transition_duration, Easing.InOutQuint); + base.OnHoverLost(state); + } } } diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index cc0123dabc..e767f6ec83 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -27,8 +27,6 @@ namespace osu.Game.Overlays.Direct { public readonly BeatmapSetInfo SetInfo; - protected Box BlackBackground; - private const double hover_transition_time = 400; private Container content; @@ -81,12 +79,6 @@ namespace osu.Game.Overlays.Direct EdgeEffect = edgeEffectNormal, Children = new[] { - // temporary blackness until the actual background loads. - BlackBackground = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, CreateBackground(), progressBar = new ProgressBar { @@ -155,22 +147,6 @@ namespace osu.Game.Overlays.Direct protected void ShowInformation() => beatmapSetOverlay?.ShowBeatmapSet(SetInfo); - protected void StartDownload() - { - if (beatmaps.GetExistingDownload(SetInfo) != null) - { - // we already have an active download running. - content.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(SetInfo); - } - private void attachDownload(DownloadBeatmapSetRequest request) { if (request.BeatmapSet.OnlineBeatmapSetID != SetInfo.OnlineBeatmapSetID) @@ -215,21 +191,10 @@ 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 => - { - d.FadeInFromZero(400, Easing.Out); - BlackBackground.Delay(400).FadeOut(); - }, - }, 300) + protected Drawable CreateBackground() => new UpdateableBeatmapSetCover { RelativeSizeAxes = Axes.Both, + BeatmapSet = SetInfo, }; public class Statistic : FillFlowContainer diff --git a/osu.Game/Overlays/Direct/DownloadButton.cs b/osu.Game/Overlays/Direct/DownloadButton.cs index f01c9dac59..1ffa8dbd35 100644 --- a/osu.Game/Overlays/Direct/DownloadButton.cs +++ b/osu.Game/Overlays/Direct/DownloadButton.cs @@ -3,6 +3,8 @@ using osu.Framework.Graphics; using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using OpenTK; @@ -13,10 +15,12 @@ namespace osu.Game.Overlays.Direct { private readonly SpriteIcon icon; - public DownloadButton() + public DownloadButton(BeatmapSetInfo set, bool noVideo = false) { + BeatmapSetDownloader downloader; Children = new Drawable[] { + downloader = new BeatmapSetDownloader(set, noVideo), icon = new SpriteIcon { Anchor = Anchor.Centre, @@ -25,6 +29,25 @@ namespace osu.Game.Overlays.Direct Icon = FontAwesome.fa_osu_chevron_down_o, }, }; + + Action = () => + { + if (!downloader.Download()) + { + Content.MoveToX(-5, 50, Easing.OutSine).Then() + .MoveToX(5, 100, Easing.InOutSine).Then() + .MoveToX(-5, 100, Easing.InOutSine).Then() + .MoveToX(0, 50, Easing.InSine); + } + }; + + downloader.Downloaded.ValueChanged += d => + { + if (d) + this.FadeOut(200); + else + this.FadeIn(200); + }; } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/Direct/PlayButton.cs index 4913b11ae1..131083c6ff 100644 --- a/osu.Game/Overlays/Direct/PlayButton.cs +++ b/osu.Game/Overlays/Direct/PlayButton.cs @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Direct return; } - icon.Icon = playing ? FontAwesome.fa_pause : FontAwesome.fa_play; + icon.Icon = playing ? FontAwesome.fa_stop : FontAwesome.fa_play; icon.FadeColour(playing || IsHovered ? hoverColour : Color4.White, 120, Easing.InOutQuint); if (playing) diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index f437546888..b33f271986 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -28,7 +28,6 @@ namespace osu.Game.Overlays private APIAccess api; private RulesetStore rulesets; - private BeatmapManager beatmaps; private readonly FillFlowContainer resultCountsContainer; private readonly OsuSpriteText resultCountsText; @@ -177,24 +176,14 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(OsuColour colours, APIAccess api, RulesetStore rulesets, BeatmapManager beatmaps) + private void load(OsuColour colours, APIAccess api, RulesetStore rulesets) { this.api = api; this.rulesets = rulesets; - this.beatmaps = beatmaps; resultCountsContainer.Colour = colours.Yellow; - - beatmaps.ItemAdded += setAdded; } - private void setAdded(BeatmapSetInfo set) => Schedule(() => - { - // if a new map was imported, we should remove it from search results (download completed etc.) - panels?.FirstOrDefault(p => p.SetInfo.OnlineBeatmapSetID == set.OnlineBeatmapSetID)?.FadeOut(400).Expire(); - BeatmapSets = BeatmapSets?.Where(b => b.OnlineBeatmapSetID != set.OnlineBeatmapSetID); - }); - private void updateResultCounts() { resultCountsContainer.FadeTo(ResultAmounts == null ? 0f : 1f, 200, Easing.OutQuint); @@ -297,9 +286,7 @@ namespace osu.Game.Overlays { Task.Run(() => { - var onlineIds = response.Select(r => r.OnlineBeatmapSetID).ToList(); - var presentOnlineIds = beatmaps.QueryBeatmapSets(s => onlineIds.Contains(s.OnlineBeatmapSetID) && !s.DeletePending).Select(r => r.OnlineBeatmapSetID).ToList(); - var sets = response.Select(r => r.ToBeatmapSet(rulesets)).Where(b => !presentOnlineIds.Contains(b.OnlineBeatmapSetID)).ToList(); + var sets = response.Select(r => r.ToBeatmapSet(rulesets)).ToList(); // may not need scheduling; loads async internally. Schedule(() => @@ -323,14 +310,6 @@ namespace osu.Game.Overlays private int distinctCount(List list) => list.Distinct().ToArray().Length; - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (beatmaps != null) - beatmaps.ItemAdded -= setAdded; - } - public class ResultCounts { public readonly int Artists; diff --git a/osu.Game/Overlays/HoldToConfirmOverlay.cs b/osu.Game/Overlays/HoldToConfirmOverlay.cs index a0e4bf1a39..7e2f6f5891 100644 --- a/osu.Game/Overlays/HoldToConfirmOverlay.cs +++ b/osu.Game/Overlays/HoldToConfirmOverlay.cs @@ -1,11 +1,10 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; using OpenTK.Graphics; namespace osu.Game.Overlays @@ -14,22 +13,10 @@ namespace osu.Game.Overlays /// An overlay which will display a black screen that dims over a period before confirming an exit action. /// Action is BYO (derived class will need to call and from a user event). /// - public abstract class HoldToConfirmOverlay : Container + public abstract class HoldToConfirmOverlay : HoldToConfirmContainer { - public Action Action; - private Box overlay; - private const int activate_delay = 400; - private const int fadeout_delay = 200; - - private bool fired; - - /// - /// Whether the overlay should be allowed to return from a fired state. - /// - protected virtual bool AllowMultipleFires => false; - [BackgroundDependencyLoader] private void load() { @@ -45,22 +32,8 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, } }; - } - protected void BeginConfirm() - { - if (!AllowMultipleFires && fired) return; - overlay.FadeIn(activate_delay * (1 - overlay.Alpha), Easing.Out).OnComplete(_ => - { - Action?.Invoke(); - fired = true; - }); - } - - protected void AbortConfirm() - { - if (!AllowMultipleFires && fired) return; - overlay.FadeOut(fadeout_delay, Easing.Out); + Progress.ValueChanged += v => overlay.Alpha = (float)v; } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 7406a9ec4f..ffe1560627 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -180,7 +180,7 @@ namespace osu.Game.Overlays.KeyBinding return true; } - protected override bool OnWheel(InputState state) + protected override bool OnScroll(InputState state) { if (HasFocus) { @@ -192,7 +192,7 @@ namespace osu.Game.Overlays.KeyBinding } } - return base.OnWheel(state); + return base.OnScroll(state); } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 632c00d1fd..f1624721da 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Mods protected Color4 LowMultiplierColour, HighMultiplierColour; protected readonly TriangleButton DeselectAllButton; - protected readonly OsuSpriteText MultiplierLabel; + protected readonly OsuSpriteText MultiplierLabel, UnrankedLabel; private readonly FillFlowContainer footerContainer; protected override bool BlockPassThroughKeyboard => false; @@ -58,6 +58,7 @@ namespace osu.Game.Overlays.Mods LowMultiplierColour = colours.Red; HighMultiplierColour = colours.Green; + UnrankedLabel.Colour = colours.Blue; if (osu != null) Ruleset.BindTo(osu.Ruleset); @@ -99,15 +100,14 @@ namespace osu.Game.Overlays.Mods } MultiplierLabel.Text = $"{multiplier:N2}x"; - if (!ranked) - MultiplierLabel.Text += " (Unranked)"; - if (multiplier > 1.0) MultiplierLabel.FadeColour(HighMultiplierColour, 200); else if (multiplier < 1.0) MultiplierLabel.FadeColour(LowMultiplierColour, 200); else MultiplierLabel.FadeColour(Color4.White, 200); + + UnrankedLabel.FadeTo(ranked ? 0 : 1, 200); } protected override void PopOut() @@ -352,23 +352,33 @@ namespace osu.Game.Overlays.Mods }, new OsuSpriteText { - Text = @"Score Multiplier: ", + Text = @"Score Multiplier:", TextSize = 30, - Shadow = true, Margin = new MarginPadding { - Top = 5 + Top = 5, + Right = 10 } }, MultiplierLabel = new OsuSpriteText { Font = @"Exo2.0-Bold", TextSize = 30, - Shadow = true, Margin = new MarginPadding { Top = 5 } + }, + UnrankedLabel = new OsuSpriteText + { + Font = @"Exo2.0-Bold", + Text = @"(Unranked)", + TextSize = 30, + Margin = new MarginPadding + { + Top = 5, + Left = 10 + } } } } diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 8c8ff89420..19ce0cf932 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -73,13 +73,13 @@ namespace osu.Game.Overlays.Music } [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, OsuGameBase osuGame) + private void load(BeatmapManager beatmaps, IBindableBeatmap beatmap) { beatmaps.GetAllUsableBeatmapSets().ForEach(addBeatmapSet); beatmaps.ItemAdded += addBeatmapSet; beatmaps.ItemRemoved += removeBeatmapSet; - beatmapBacking.BindTo(osuGame.Beatmap); + beatmapBacking.BindTo(beatmap); beatmapBacking.ValueChanged += _ => updateSelectedSet(); } @@ -101,7 +101,7 @@ namespace osu.Game.Overlays.Music private void updateSelectedSet() { foreach (PlaylistItem s in items.Children) - s.Selected = s.BeatmapSetInfo.ID == beatmapBacking.Value.BeatmapSetInfo.ID; + s.Selected = s.BeatmapSetInfo.ID == beatmapBacking.Value.BeatmapSetInfo?.ID; } public string SearchTerm diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 76c2222f8b..b74e7e1178 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -21,17 +21,22 @@ namespace osu.Game.Overlays.Music private const float transition_duration = 600; private const float playlist_height = 510; + /// + /// Invoked when the order of an item in the list has changed. + /// The second parameter indicates the new index of the item. + /// public Action OrderChanged; + private readonly Bindable beatmap = new Bindable(); private BeatmapManager beatmaps; + private FilterControl filter; private PlaylistList list; - private readonly Bindable beatmapBacking = new Bindable(); - [BackgroundDependencyLoader] - private void load(OsuGameBase game, BeatmapManager beatmaps, OsuColour colours) + private void load(OsuColour colours, BindableBeatmap beatmap, BeatmapManager beatmaps) { + this.beatmap.BindTo(beatmap); this.beatmaps = beatmaps; Children = new Drawable[] @@ -73,13 +78,14 @@ namespace osu.Game.Overlays.Music }, }; - beatmapBacking.BindTo(game.Beatmap); - filter.Search.OnCommit = (sender, newText) => { - BeatmapInfo beatmap = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); - if (beatmap != null) - beatmapBacking.Value = beatmaps.GetWorkingBeatmap(beatmap); + BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); + if (toSelect != null) + { + beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect); + beatmap.Value.Track.Restart(); + } }; } @@ -102,13 +108,14 @@ namespace osu.Game.Overlays.Music private void itemSelected(BeatmapSetInfo set) { - if (set.ID == (beatmapBacking.Value?.BeatmapSetInfo?.ID ?? -1)) + if (set.ID == (beatmap.Value?.BeatmapSetInfo?.ID ?? -1)) { - beatmapBacking.Value?.Track?.Seek(0); + beatmap.Value?.Track?.Seek(0); return; } - beatmapBacking.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); + beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); + beatmap.Value.Track.Restart(); } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index a21b34b352..f7f0d7ec6a 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -30,11 +30,8 @@ namespace osu.Game.Overlays public class MusicController : OsuFocusedOverlayContainer { private const float player_height = 130; - private const float transition_length = 800; - private const float progress_height = 10; - private const float bottom_black_area_height = 55; private Drawable background; @@ -49,16 +46,17 @@ namespace osu.Game.Overlays private PlaylistOverlay playlist; + private BeatmapManager beatmaps; private LocalisationEngine localisation; - private BeatmapManager beatmaps; - private readonly Bindable beatmapBacking = new Bindable(); private List beatmapSets; private BeatmapSetInfo currentSet; private Container dragContainer; private Container playerContainer; + private readonly Bindable beatmap = new Bindable(); + public MusicController() { Width = 400; @@ -68,43 +66,16 @@ namespace osu.Game.Overlays AlwaysPresent = true; } - private Vector2 dragStart; - - protected override bool OnDragStart(InputState state) - { - base.OnDragStart(state); - dragStart = state.Mouse.Position; - return true; - } - - protected override bool OnDrag(InputState state) - { - if (base.OnDrag(state)) return true; - - Vector2 change = state.Mouse.Position - dragStart; - - // Diminish the drag distance as we go further to simulate "rubber band" feeling. - change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length; - - dragContainer.MoveTo(change); - return true; - } - - protected override bool OnDragEnd(InputState state) - { - dragContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic); - return base.OnDragEnd(state); - } - [BackgroundDependencyLoader] - private void load(OsuGameBase game, BeatmapManager beatmaps, OsuColour colours, LocalisationEngine localisation) + private void load(BindableBeatmap beatmap, BeatmapManager beatmaps, OsuColour colours, LocalisationEngine localisation) { + this.beatmap.BindTo(beatmap); this.beatmaps = beatmaps; this.localisation = localisation; Children = new Drawable[] { - dragContainer = new Container + dragContainer = new DragContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -224,8 +195,6 @@ namespace osu.Game.Overlays beatmaps.ItemAdded += handleBeatmapAdded; beatmaps.ItemRemoved += handleBeatmapRemoved; - beatmapBacking.BindTo(game.Beatmap); - playlist.StateChanged += s => playlistButton.FadeColour(s == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); } @@ -246,10 +215,8 @@ namespace osu.Game.Overlays protected override void LoadComplete() { - beatmapBacking.ValueChanged += beatmapChanged; - beatmapBacking.DisabledChanged += beatmapDisabledChanged; - beatmapBacking.TriggerChange(); - + beatmap.BindValueChanged(beatmapChanged, true); + beatmap.BindDisabledChanged(beatmapDisabledChanged, true); base.LoadComplete(); } @@ -286,7 +253,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 && beatmapSets.Any()) + if (track.HasCompleted && !track.Looping && !beatmap.Disabled && beatmapSets.Any()) next(); } else @@ -299,7 +266,7 @@ namespace osu.Game.Overlays if (track == null) { - if (!beatmapBacking.Disabled) + if (!beatmap.Disabled) next(true); return; } @@ -317,8 +284,8 @@ namespace osu.Game.Overlays var playable = beatmapSets.TakeWhile(i => i.ID != current.BeatmapSetInfo.ID).LastOrDefault() ?? beatmapSets.LastOrDefault(); if (playable != null) { - beatmapBacking.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmapBacking); - beatmapBacking.Value.Track.Restart(); + beatmap.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); + beatmap.Value.Track.Restart(); } } @@ -330,8 +297,8 @@ namespace osu.Game.Overlays var playable = beatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).Skip(1).FirstOrDefault() ?? beatmapSets.FirstOrDefault(); if (playable != null) { - beatmapBacking.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmapBacking); - beatmapBacking.Value.Track.Restart(); + beatmap.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); + beatmap.Value.Track.Restart(); } } @@ -485,5 +452,36 @@ namespace osu.Game.Overlays sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4"); } } + + private class DragContainer : Container + { + private Vector2 dragStart; + + protected override bool OnDragStart(InputState state) + { + base.OnDragStart(state); + dragStart = state.Mouse.Position; + return true; + } + + protected override bool OnDrag(InputState state) + { + if (base.OnDrag(state)) return true; + + Vector2 change = state.Mouse.Position - dragStart; + + // Diminish the drag distance as we go further to simulate "rubber band" feeling. + change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length; + + this.MoveTo(change); + return true; + } + + protected override bool OnDragEnd(InputState state) + { + this.MoveTo(Vector2.Zero, 800, Easing.OutElastic); + return base.OnDragEnd(state); + } + } } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 09b6022ac5..3dc8f5ec15 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -22,11 +22,6 @@ namespace osu.Game.Overlays public const float TRANSITION_LENGTH = 600; - /// - /// Whether posted notifications should be processed. - /// - public readonly BindableBool Enabled = new BindableBool(true); - private FlowContainer sections; /// @@ -34,27 +29,6 @@ namespace osu.Game.Overlays /// public Func GetToolbarHeight; - public NotificationOverlay() - { - ScheduledDelegate notificationsEnabler = null; - Enabled.ValueChanged += v => - { - if (!IsLoaded) - { - processingPosts = v; - return; - } - - notificationsEnabler?.Cancel(); - - if (v) - // we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed. - notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, 1000); - else - processingPosts = false; - }; - } - [BackgroundDependencyLoader] private void load() { @@ -103,6 +77,29 @@ namespace osu.Game.Overlays }; } + private ScheduledDelegate notificationsEnabler; + private void updateProcessingMode() + { + bool enabled = OverlayActivationMode == OverlayActivation.All || State == Visibility.Visible; + + notificationsEnabler?.Cancel(); + + if (enabled) + // we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed. + notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State == Visibility.Visible ? 0 : 1000); + else + processingPosts = false; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + StateChanged += _ => updateProcessingMode(); + OverlayActivationMode.ValueChanged += _ => updateProcessingMode(); + OverlayActivationMode.TriggerChange(); + } + private int totalCount => sections.Select(c => c.DisplayedCount).Sum(); private int unreadCount => sections.Select(c => c.UnreadCount).Sum(); diff --git a/osu.Game/Overlays/OverlayActivation.cs b/osu.Game/Overlays/OverlayActivation.cs new file mode 100644 index 0000000000..da4e153ce9 --- /dev/null +++ b/osu.Game/Overlays/OverlayActivation.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Overlays +{ + public enum OverlayActivation + { + Disabled, + UserTriggered, + All + } +} diff --git a/osu.Game/Overlays/Profile/Header/BadgeContainer.cs b/osu.Game/Overlays/Profile/Header/BadgeContainer.cs index 36a9a9b01a..f968f94187 100644 --- a/osu.Game/Overlays/Profile/Header/BadgeContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BadgeContainer.cs @@ -107,13 +107,20 @@ namespace osu.Game.Overlays.Profile.Header visibleBadge = 0; badgeFlowContainer.Clear(); - foreach (var badge in badges) + for (var index = 0; index < badges.Length; index++) { - LoadComponentAsync(new DrawableBadge(badge) + int displayIndex = index; + LoadComponentAsync(new DrawableBadge(badges[index]) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - }, badgeFlowContainer.Add); + }, asyncBadge => + { + badgeFlowContainer.Add(asyncBadge); + + // load in stable order regardless of async load order. + badgeFlowContainer.SetLayoutPosition(asyncBadge, displayIndex); + }); } } diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 4c411b3210..067144c26e 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Diagnostics; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; @@ -10,13 +9,13 @@ 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.Textures; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Profile.Header; using osu.Game.Users; @@ -105,11 +104,28 @@ namespace osu.Game.Overlays.Profile Y = -75, Size = new Vector2(25, 25) }, - new ProfileLink(user) + new FillFlowContainer { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Y = -48, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = user.Username, + Font = @"Exo2.0-RegularItalic", + TextSize = 30, + }, + new ExternalLinkButton($@"https://osu.ppy.sh/users/{user.Id}") + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Left = 3, Bottom = 3 }, //To better lineup with the font + }, + } }, countryFlag = new DrawableFlag(user.Country) { @@ -455,28 +471,6 @@ namespace osu.Game.Overlays.Profile infoTextRight.NewLine(); } - private class ProfileLink : OsuHoverContainer, IHasTooltip - { - public string TooltipText => "View Profile in Browser"; - - public override bool HandleMouseInput => true; - - public ProfileLink(User user) - { - Action = () => Process.Start($@"https://osu.ppy.sh/users/{user.Id}"); - - AutoSizeAxes = Axes.Both; - - Child = new OsuSpriteText - { - Text = user.Username, - Font = @"Exo2.0-RegularItalic", - TextSize = 30, - }; - } - } - - private class GradeBadge : Container { private const float width = 50; diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs index 97079c77f3..359bfc7564 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Profile.Sections { Action = () => { - if (beatmap.OnlineBeatmapSetID.HasValue) beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.OnlineBeatmapSetID.Value); + if (beatmap.BeatmapSet?.OnlineBeatmapSetID != null) beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.BeatmapSet.OnlineBeatmapSetID.Value); }; Child = new FillFlowContainer diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs index be8e09105b..0a2b2fe121 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedRow.cs @@ -24,19 +24,13 @@ namespace osu.Game.Overlays.Profile.Sections.Historical this.playCount = playCount; } - protected override Drawable CreateLeftVisual() => new DelayedLoadWrapper(new BeatmapSetCover(beatmap.BeatmapSet, BeatmapSetCoverType.List) + protected override Drawable CreateLeftVisual() => new UpdateableBeatmapSetCover { - 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, + Origin = Anchor.CentreLeft, Size = new Vector2(80, 50), + BeatmapSet = beatmap.BeatmapSet, + CoverType = BeatmapSetCoverType.List, }; [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 1e0406c125..6dbb9b9ba3 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -64,6 +64,7 @@ namespace osu.Game.Overlays.Profile.Sections { TextSize = 14, Text = "show more", + Padding = new MarginPadding {Vertical = 10, Horizontal = 15 }, } }, ShowMoreLoading = new LoadingAnimation diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 87df84e2e5..707de0a10f 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -8,6 +8,7 @@ using osu.Game.Online.API.Requests; using osu.Game.Users; using System; using System.Linq; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Profile.Sections.Ranks { @@ -49,7 +50,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks MissingText.Hide(); - foreach (OnlineScore score in scores) + foreach (APIScore score in scores) { DrawableProfileScore drawableScore; diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 721cc30b61..046c1b1a33 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -8,6 +8,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Chat; using osu.Game.Screens.Select.Leaderboards; @@ -17,11 +18,11 @@ namespace osu.Game.Overlays.Profile.Sections.Recent { private APIAccess api; - private readonly RecentActivity activity; + private readonly APIRecentActivity activity; private LinkFlowContainer content; - public DrawableRecentActivity(RecentActivity activity) + public DrawableRecentActivity(APIRecentActivity activity) { this.activity = activity; } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index 8c1108b115..ee2f2f5973 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Online.API.Requests; using osu.Game.Users; using System.Linq; +using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Profile.Sections.Recent { @@ -36,7 +37,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent MissingText.Hide(); - foreach (RecentActivity activity in activities) + foreach (APIRecentActivity activity in activities) { ItemsContainer.Add(new DrawableRecentActivity(activity)); } diff --git a/osu.Game/Overlays/Settings/RulesetSettingsSubsection.cs b/osu.Game/Overlays/Settings/RulesetSettingsSubsection.cs new file mode 100644 index 0000000000..05104018cd --- /dev/null +++ b/osu.Game/Overlays/Settings/RulesetSettingsSubsection.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Game.Rulesets; + +namespace osu.Game.Overlays.Settings +{ + /// + /// A which provides subclasses with the + /// from the 's . + /// + public abstract class RulesetSettingsSubsection : SettingsSubsection + { + private readonly Ruleset ruleset; + + protected RulesetSettingsSubsection(Ruleset ruleset) + { + this.ruleset = ruleset; + } + + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + + var config = dependencies.Get().GetConfigFor(ruleset); + if (config != null) + dependencies.Cache(config); + + return dependencies; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 647395cf69..9c8f5e2643 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Overlays.Settings.Sections.Gameplay { @@ -38,6 +39,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Always show key overlay", Bindable = config.GetBindable(OsuSetting.KeyOverlay) }, + new SettingsEnumDropdown + { + LabelText = "Score display mode", + Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) + } }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs new file mode 100644 index 0000000000..a9cefa81da --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Game.Configuration; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class ModsSettings : SettingsSubsection + { + protected override string Header => "Mods"; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new[] + { + new SettingsCheckbox + { + LabelText = "Increase visibility of first object with \"Hidden\" mod", + Bindable = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index 3851a73901..8add0b01ec 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -21,7 +21,8 @@ namespace osu.Game.Overlays.Settings.Sections { new GeneralSettings(), new SongSelectSettings(), - new ScrollingSettings() + new ScrollingSettings(), + new ModsSettings(), }; } diff --git a/osu.Game/Overlays/Settings/SettingsFooter.cs b/osu.Game/Overlays/Settings/SettingsFooter.cs index 900f03fe7b..909fc20446 100644 --- a/osu.Game/Overlays/Settings/SettingsFooter.cs +++ b/osu.Game/Overlays/Settings/SettingsFooter.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -12,6 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; using OpenTK; using OpenTK.Graphics; +using DebugUtils = osu.Game.Utils.DebugUtils; namespace osu.Game.Overlays.Settings { diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 424a457110..48d0674b3d 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -10,6 +10,8 @@ using osu.Framework.Input; using osu.Game.Graphics; using OpenTK; using osu.Framework.Graphics.Shapes; +using osu.Framework.Allocation; +using osu.Framework.Configuration; namespace osu.Game.Overlays.Toolbar { @@ -29,6 +31,8 @@ namespace osu.Game.Overlays.Toolbar private const float alpha_hovering = 0.8f; private const float alpha_normal = 0.6f; + private readonly Bindable overlayActivationMode = new Bindable(OverlayActivation.All); + public Toolbar() { Children = new Drawable[] @@ -76,6 +80,19 @@ namespace osu.Game.Overlays.Toolbar Size = new Vector2(1, HEIGHT); } + [BackgroundDependencyLoader(true)] + private void load(OsuGame osuGame) + { + if (osuGame != null) + overlayActivationMode.BindTo(osuGame.OverlayActivationMode); + + StateChanged += visibility => + { + if (overlayActivationMode == OverlayActivation.Disabled) + State = Visibility.Hidden; + }; + } + public class ToolbarBackground : Container { private readonly Box solidBackground; diff --git a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs index 1da51e4a5a..3078c44844 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarModeSelector.cs @@ -6,7 +6,9 @@ using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using OpenTK; +using OpenTK.Input; using OpenTK.Graphics; using osu.Framework.Configuration; using osu.Framework.Graphics.Shapes; @@ -22,6 +24,7 @@ namespace osu.Game.Overlays.Toolbar private readonly Drawable modeButtonLine; private ToolbarModeButton activeButton; + private RulesetStore rulesets; private readonly Bindable ruleset = new Bindable(); public ToolbarModeSelector() @@ -67,26 +70,42 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader(true)] private void load(RulesetStore rulesets, OsuGame game) { + this.rulesets = rulesets; foreach (var r in rulesets.AvailableRulesets) { modeButtons.Add(new ToolbarModeButton { Ruleset = r, - Action = delegate - { - ruleset.Value = r; - } + Action = delegate { ruleset.Value = r; } }); } ruleset.ValueChanged += rulesetChanged; ruleset.DisabledChanged += disabledChanged; + if (game != null) ruleset.BindTo(game.Ruleset); else ruleset.Value = rulesets.AvailableRulesets.FirstOrDefault(); } + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + base.OnKeyDown(state, args); + + if (state.Keyboard.ControlPressed && !args.Repeat && args.Key >= Key.Number1 && args.Key <= Key.Number9) + { + int requested = args.Key - Key.Number1; + + RulesetInfo found = rulesets.AvailableRulesets.Skip(requested).FirstOrDefault(); + if (found != null) + ruleset.Value = found; + return true; + } + + return false; + } + public override bool HandleKeyboardInput => !ruleset.Disabled && base.HandleKeyboardInput; public override bool HandleMouseInput => !ruleset.Disabled && base.HandleMouseInput; diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 64106967f4..0e43945f8c 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Volume public class VolumeMeter : Container, IKeyBindingHandler { private CircularProgress volumeCircle; + private CircularProgress volumeCircleGlow; + public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; private readonly float circleSize; private readonly Color4 meterColour; @@ -44,90 +46,143 @@ namespace osu.Game.Overlays.Volume [BackgroundDependencyLoader] private void load(OsuColour colours) { - Add(new Container - { - Size = new Vector2(120, 20), - CornerRadius = 10, - Masking = true, - Margin = new MarginPadding { Left = circleSize + 10 }, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray1, - Alpha = 0.9f, - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = "Exo2.0-Bold", - Text = name - } - } - }); + Color4 backgroundColour = colours.Gray1; CircularProgress bgProgress; - Add(new CircularContainer + const float progress_start_radius = 0.75f; + const float progress_size = 0.03f; + const float progress_end_radius = progress_start_radius + progress_size; + + const float blur_amount = 5; + + Children = new Drawable[] { - Masking = true, - Size = new Vector2(circleSize), - Children = new Drawable[] + new Container { - new Box + Size = new Vector2(circleSize), + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray1, - Alpha = 0.9f, - }, - bgProgress = new CircularProgress - { - RelativeSizeAxes = Axes.Both, - InnerRadius = 0.05f, - Rotation = 180, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = colours.Gray2, - Size = new Vector2(0.8f) - }, - new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.8f), - Padding = new MarginPadding(-Blur.KernelSize(5)), - Rotation = 180, - Child = (volumeCircle = new CircularProgress + new BufferedContainer { + Alpha = 0.9f, RelativeSizeAxes = Axes.Both, - InnerRadius = 0.05f, + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour, + }, + new CircularContainer + { + Masking = true, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(progress_end_radius), + Children = new Drawable[] + { + bgProgress = new CircularProgress + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Rotation = 180, + Colour = backgroundColour, + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Name = "Progress under covers for smoothing", + RelativeSizeAxes = Axes.Both, + Rotation = 180, + Child = volumeCircle = new CircularProgress + { + RelativeSizeAxes = Axes.Both, + } + }, + } + }, + new Circle + { + Name = "Inner Cover", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour, + Size = new Vector2(progress_start_radius), + }, + new Container + { + Name = "Progress overlay for glow", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(progress_start_radius + progress_size / 1.5f), + Rotation = 180, + Padding = new MarginPadding(-Blur.KernelSize(blur_amount)), + Child = (volumeCircleGlow = new CircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = progress_size * 0.8f, + }).WithEffect(new GlowEffect + { + Colour = meterColour, + BlurSigma = new Vector2(blur_amount), + Strength = 5, + PadExtent = true + }), + }, + }, + }, + maxGlow = (text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = "Venera", + TextSize = 0.16f * circleSize }).WithEffect(new GlowEffect { - Colour = meterColour, - Strength = 2, - PadExtent = true - }), - }, - maxGlow = (text = new OsuSpriteText + Colour = Color4.Transparent, + PadExtent = true, + }) + } + }, + new Container + { + Size = new Vector2(120, 20), + CornerRadius = 10, + Masking = true, + Margin = new MarginPadding { Left = circleSize + 10 }, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = "Venera", - TextSize = 0.16f * circleSize - }).WithEffect(new GlowEffect - { - Colour = Color4.Transparent, - PadExtent = true, - }) + new Box + { + Alpha = 0.9f, + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = "Exo2.0-Bold", + Text = name + } + } } - }); - - Bindable.ValueChanged += newVolume => { this.TransformTo("DisplayVolume", newVolume, 400, Easing.OutQuint); }; + }; + Bindable.ValueChanged += newVolume => + { + this.TransformTo("DisplayVolume", + newVolume, + 400, + Easing.OutQuint); + }; bgProgress.Current.Value = 0.75f; } @@ -158,6 +213,7 @@ namespace osu.Game.Overlays.Volume } volumeCircle.Current.Value = displayVolume * 0.75f; + volumeCircleGlow.Current.Value = displayVolume * 0.75f; } } @@ -167,9 +223,25 @@ namespace osu.Game.Overlays.Volume private set => Bindable.Value = value; } - public void Increase() => Volume += 0.05f; + private const float adjust_step = 0.05f; - public void Decrease() => Volume -= 0.05f; + public void Increase() => adjust(1); + public void Decrease() => adjust(-1); + + private void adjust(int direction) + { + float amount = adjust_step * direction; + + var mouse = GetContainingInputManager().CurrentState.Mouse; + if (mouse.HasPreciseScroll) + { + float scrollDelta = mouse.ScrollDelta.Y; + if (scrollDelta != 0) + amount *= Math.Abs(scrollDelta / 10); + } + + Volume += amount; + } public bool OnPressed(GlobalAction action) { diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index 4ecf1eefb2..74cece5154 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -8,7 +8,8 @@ namespace osu.Game.Rulesets.Configuration public abstract class RulesetConfigManager : DatabasedConfigManager, IRulesetConfigManager where T : struct { - protected RulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int variant) : base(settings, ruleset, variant) + protected RulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null) + : base(settings, ruleset, variant) { } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs new file mode 100644 index 0000000000..1fdebd586f --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Difficulty +{ + public class DifficultyAttributes + { + public readonly Mod[] Mods; + public readonly double StarRating; + + public DifficultyAttributes(Mod[] mods, double starRating) + { + Mods = mods; + StarRating = starRating; + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 070bc7ddb0..8f9651ab09 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Extensions.IEnumerableExtensions; @@ -12,30 +13,91 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { - protected readonly IBeatmap Beatmap; - protected readonly Mod[] Mods; + private readonly Ruleset ruleset; + private readonly WorkingBeatmap beatmap; - protected double TimeRate { get; private set; } = 1; - - protected DifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) + protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) { - Beatmap = beatmap; - Mods = mods ?? new Mod[0]; - - ApplyMods(Mods); + this.ruleset = ruleset; + this.beatmap = beatmap; } - protected virtual void ApplyMods(Mod[] mods) + /// + /// Calculates the difficulty of the beatmap using a specific mod combination. + /// + /// The mods that should be applied to the beatmap. + /// A structure describing the difficulty of the beatmap. + public DifficultyAttributes Calculate(params Mod[] mods) { + beatmap.Mods.Value = mods; + IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo); + var clock = new StopwatchClock(); mods.OfType().ForEach(m => m.ApplyToClock(clock)); - TimeRate = clock.Rate; + + return Calculate(playableBeatmap, mods, clock.Rate); } - protected virtual void PreprocessHitObjects() + /// + /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap. + /// + /// A collection of structures describing the difficulty of the beatmap for each mod combination. + public IEnumerable CalculateAll() { + foreach (var combination in CreateDifficultyAdjustmentModCombinations()) + { + if (combination is MultiMod multi) + yield return Calculate(multi.Mods); + else + yield return Calculate(combination); + } } - public abstract double Calculate(Dictionary categoryDifficulty = null); + /// + /// Creates all combinations which adjust the difficulty. + /// + public Mod[] CreateDifficultyAdjustmentModCombinations() + { + return createDifficultyAdjustmentModCombinations(Enumerable.Empty(), DifficultyAdjustmentMods).ToArray(); + + IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) + { + // Initial-case: Empty current set + if (currentSetCount == 0) + yield return new NoModMod(); + + if (currentSetCount == 1) + yield return currentSet.Single(); + + if (currentSetCount > 1) + yield return new MultiMod(currentSet.ToArray()); + + // Apply mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod + // combinations in further recursions, so a moving subset is used to eliminate this effect + for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++) + { + var adjustmentMod = adjustmentSet[i]; + if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)))) + continue; + + foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1)) + yield return combo; + } + } + } + + /// + /// Retrieves all s which adjust the difficulty. + /// + protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty(); + + /// + /// Calculates the difficulty of a using a specific combination. + /// + /// The to compute the difficulty for. + /// The s that should be applied. + /// The rate of time in . + /// A structure containing the difficulty attributes. + protected abstract DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate); } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index 07d9c80061..ba783ee87b 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -13,8 +13,7 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class PerformanceCalculator { - private readonly Dictionary attributes = new Dictionary(); - protected IDictionary Attributes => attributes; + protected readonly DifficultyAttributes Attributes; protected readonly Ruleset Ruleset; protected readonly IBeatmap Beatmap; @@ -22,14 +21,15 @@ namespace osu.Game.Rulesets.Difficulty protected double TimeRate { get; private set; } = 1; - protected PerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) + protected PerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score) { Ruleset = ruleset; - Beatmap = beatmap; Score = score; - var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); - diffCalc.Calculate(attributes); + beatmap.Mods.Value = score.Mods; + Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo); + + Attributes = ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods); ApplyMods(score.Mods); } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 5f1b9a6bad..0c91c9f548 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Edit private RulesetContainer rulesetContainer; private readonly List layerContainers = new List(); - private readonly Bindable beatmap = new Bindable(); + private readonly IBindable beatmap = new Bindable(); protected HitObjectComposer(Ruleset ruleset) { @@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Edit } [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame, IFrameBasedClock framedClock) + private void load(IBindableBeatmap beatmap, IFrameBasedClock framedClock) { - beatmap.BindTo(osuGame.Beatmap); + this.beatmap.BindTo(beatmap); try { diff --git a/osu.Game/Rulesets/Edit/HitObjectMask.cs b/osu.Game/Rulesets/Edit/HitObjectMask.cs index ad7c27ad80..61fb700dd3 100644 --- a/osu.Game/Rulesets/Edit/HitObjectMask.cs +++ b/osu.Game/Rulesets/Edit/HitObjectMask.cs @@ -6,6 +6,7 @@ using osu.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Input; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects.Drawables; using OpenTK; @@ -137,10 +138,4 @@ namespace osu.Game.Rulesets.Edit /// public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; } - - public enum SelectionState - { - NotSelected, - Selected - } } diff --git a/osu.Game/Rulesets/Mods/IReadFromConfig.cs b/osu.Game/Rulesets/Mods/IReadFromConfig.cs new file mode 100644 index 0000000000..93c9ae0c34 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IReadFromConfig.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Configuration; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// An interface for mods that require reading access to the osu! configuration. + /// + public interface IReadFromConfig + { + void ReadFromConfig(OsuConfigManager config); + } +} diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 75f37b993d..7058d1bed6 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mods public override string ShortenedName => "AT"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_auto; public override string Description => "Watch a perfect automated play through the song."; - public override double ScoreMultiplier => 0; + public override double ScoreMultiplier => 1; public bool AllowFail => false; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; } diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index b489a665d9..9b09f0bd6d 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -1,16 +1,36 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Configuration; +using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using System.Collections.Generic; +using System.Linq; namespace osu.Game.Rulesets.Mods { - public abstract class ModHidden : Mod + public abstract class ModHidden : Mod, IReadFromConfig, IApplicableToDrawableHitObjects { public override string Name => "Hidden"; public override string ShortenedName => "HD"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_hidden; public override ModType Type => ModType.DifficultyIncrease; public override bool Ranked => true; + + protected Bindable IncreaseFirstObjectVisibility = new Bindable(); + + public void ReadFromConfig(OsuConfigManager config) + { + IncreaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); + } + + public virtual void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var d in drawables.Skip(IncreaseFirstObjectVisibility ? 1 : 0)) + d.ApplyCustomUpdateState += ApplyHiddenState; + } + + protected virtual void ApplyHiddenState(DrawableHitObject hitObject, ArmedState state) { } } } diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index e4137254e6..0bd9becd78 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Relax"; public override string ShortenedName => "RX"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_relax; - public override double ScoreMultiplier => 0; + public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModSuddenDeath) }; } } diff --git a/osu.Game/Rulesets/Mods/ModType.cs b/osu.Game/Rulesets/Mods/ModType.cs index 1941724879..913ba23701 100644 --- a/osu.Game/Rulesets/Mods/ModType.cs +++ b/osu.Game/Rulesets/Mods/ModType.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Mods { DifficultyReduction, DifficultyIncrease, - Special, + Special } } diff --git a/osu.Game/Rulesets/Mods/MultiMod.cs b/osu.Game/Rulesets/Mods/MultiMod.cs index 3c90a4eedb..b65773e93f 100644 --- a/osu.Game/Rulesets/Mods/MultiMod.cs +++ b/osu.Game/Rulesets/Mods/MultiMod.cs @@ -1,6 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Linq; + namespace osu.Game.Rulesets.Mods { public class MultiMod : Mod @@ -10,6 +13,13 @@ namespace osu.Game.Rulesets.Mods public override string Description => string.Empty; public override double ScoreMultiplier => 0; - public Mod[] Mods; + public Mod[] Mods { get; } + + public MultiMod(params Mod[] mods) + { + Mods = mods; + } + + public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray(); } } diff --git a/osu.Game/Rulesets/Mods/NoModMod.cs b/osu.Game/Rulesets/Mods/NoModMod.cs new file mode 100644 index 0000000000..dcab3538da --- /dev/null +++ b/osu.Game/Rulesets/Mods/NoModMod.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Indicates a type of mod that doesn't do anything. + /// + public sealed class NoModMod : Mod + { + public override string Name => "No Mod"; + public override string ShortenedName => "NM"; + public override double ScoreMultiplier => 1; + } +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 395eeab419..f818523a3d 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -15,6 +15,8 @@ using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Beatmaps.Legacy; +using osu.Game.Configuration; +using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; namespace osu.Game.Rulesets @@ -59,9 +61,9 @@ namespace osu.Game.Rulesets public virtual IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => null; - public abstract DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null); + public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap); - public virtual PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => null; + public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => null; public virtual HitObjectComposer CreateHitObjectComposer() => null; @@ -69,7 +71,13 @@ namespace osu.Game.Rulesets public abstract string Description { get; } - public virtual SettingsSubsection CreateSettings() => null; + public virtual RulesetSettingsSubsection CreateSettings() => null; + + /// + /// Creates the for this . + /// + /// The to store the settings. + public virtual IRulesetConfigManager CreateConfig(SettingsStore settings) => null; /// /// Do not override this unless you are a legacy mode. diff --git a/osu.Game/Rulesets/RulesetConfigCache.cs b/osu.Game/Rulesets/RulesetConfigCache.cs new file mode 100644 index 0000000000..7e83ba0961 --- /dev/null +++ b/osu.Game/Rulesets/RulesetConfigCache.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Configuration; +using osu.Game.Rulesets.Configuration; + +namespace osu.Game.Rulesets +{ + /// + /// A cache that provides a single per-ruleset. + /// This is done to support referring to and updating ruleset configs from multiple locations in the absence of inter-config bindings. + /// + public class RulesetConfigCache : Component + { + private readonly Dictionary configCache = new Dictionary(); + private readonly SettingsStore settingsStore; + + public RulesetConfigCache(SettingsStore settingsStore) + { + this.settingsStore = settingsStore; + } + + /// + /// Retrieves the for a . + /// + /// The to retrieve the for. + /// The defined by , null if doesn't define one. + /// If doesn't have a valid . + public IRulesetConfigManager GetConfigFor(Ruleset ruleset) + { + if (ruleset.RulesetInfo.ID == null) + throw new InvalidOperationException("The provided ruleset doesn't have a valid id."); + + if (configCache.TryGetValue(ruleset.RulesetInfo.ID.Value, out var existing)) + return existing; + + return configCache[ruleset.RulesetInfo.ID.Value] = ruleset.CreateConfig(settingsStore); + } + } +} diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 1847b63658..8d267f48e9 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using osu.Framework.Logging; using osu.Game.Database; namespace osu.Game.Rulesets @@ -114,8 +115,9 @@ namespace osu.Game.Rulesets var assembly = Assembly.LoadFrom(file); loaded_assemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset))); } - catch (Exception) + catch (Exception e) { + Logger.Error(e, "Failed to load ruleset"); } } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 345930ed04..dd4120f2fb 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -65,6 +65,11 @@ namespace osu.Game.Rulesets.Scoring /// public readonly BindableInt HighestCombo = new BindableInt(); + /// + /// The used to calculate scores. + /// + public readonly Bindable Mode = new Bindable(); + /// /// Whether all s have been processed. /// @@ -169,8 +174,6 @@ namespace osu.Game.Rulesets.Scoring private const double combo_portion = 0.7; private const double max_score = 1000000; - public readonly Bindable Mode = new Bindable(); - protected sealed override bool HasCompleted => JudgedHits == MaxHits; protected int MaxHits { get; private set; } @@ -199,16 +202,18 @@ namespace osu.Game.Rulesets.Scoring if (maxBaseScore == 0 || maxHighestCombo == 0) { - Mode.Value = ScoringMode.Exponential; + Mode.Value = ScoringMode.Classic; Mode.Disabled = true; } + + Mode.ValueChanged += _ => updateScore(); } /// /// Simulates an autoplay of s that will be judged by this /// by adding s for each in the . /// - /// This is required for to work, otherwise will be used. + /// This is required for to work, otherwise will be used. /// /// /// The containing the s that will be judged by this . @@ -295,8 +300,9 @@ namespace osu.Game.Rulesets.Scoring case ScoringMode.Standardised: TotalScore.Value = max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo / maxHighestCombo) + bonusScore; break; - case ScoringMode.Exponential: - TotalScore.Value = (baseScore + bonusScore) * Math.Log(HighestCombo + 1, 2); + case ScoringMode.Classic: + // should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1) + TotalScore.Value = bonusScore + baseScore * (1 + Math.Max(0, HighestCombo - 1) / 25); break; } } @@ -322,6 +328,6 @@ namespace osu.Game.Rulesets.Scoring public enum ScoringMode { Standardised, - Exponential + Classic } } diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 1b6841c9bd..af18d98561 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.UI { public class HitObjectContainer : CompositeDrawable { - public virtual IEnumerable Objects => InternalChildren.Cast(); - public virtual IEnumerable AliveObjects => AliveInternalChildren.Cast(); + public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); + public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); public virtual void Add(DrawableHitObject hitObject) => AddInternal(hitObject); public virtual bool Remove(DrawableHitObject hitObject) => RemoveInternal(hitObject); diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index e42e74c245..fedb6abed3 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -57,6 +57,7 @@ namespace osu.Game.Rulesets.UI public abstract IEnumerable Objects { get; } private readonly Lazy playfield; + /// /// The playfield. /// @@ -72,11 +73,6 @@ namespace osu.Game.Rulesets.UI private IRulesetConfigManager rulesetConfig; private OnScreenDisplay onScreenDisplay; - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); - /// /// A visual representation of a . /// @@ -89,18 +85,20 @@ namespace osu.Game.Rulesets.UI Cursor = CreateCursor(); } - [BackgroundDependencyLoader(true)] - private void load(OnScreenDisplay onScreenDisplay, SettingsStore settings) + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) { - this.onScreenDisplay = onScreenDisplay; + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); - rulesetConfig = CreateConfig(Ruleset, settings); + onScreenDisplay = dependencies.Get(); + rulesetConfig = dependencies.Get().GetConfigFor(Ruleset); if (rulesetConfig != null) { dependencies.Cache(rulesetConfig); onScreenDisplay?.BeginTracking(this, rulesetConfig); } + + return dependencies; } public abstract ScoreProcessor CreateScoreProcessor(); @@ -130,14 +128,11 @@ namespace osu.Game.Rulesets.UI HasReplayLoaded.Value = ReplayInputManager.ReplayInputHandler != null; } - /// /// Creates the cursor. May be null if the doesn't provide a custom cursor. /// protected virtual CursorContainer CreateCursor() => null; - protected virtual IRulesetConfigManager CreateConfig(Ruleset ruleset, SettingsStore settings) => null; - /// /// Creates a Playfield. /// @@ -160,7 +155,7 @@ namespace osu.Game.Rulesets.UI /// RulesetContainer that applies conversion to Beatmaps. Does not contain a Playfield /// and does not load drawable hit objects. /// - /// Should not be derived - derive instead. + /// Should not be derived - derive instead. /// /// /// The type of HitObject contained by this RulesetContainer. @@ -194,6 +189,7 @@ namespace osu.Game.Rulesets.UI protected override Container Content => content; private Container content; + private IEnumerable mods; /// /// Whether to assume the beatmap passed into this is for the current ruleset. @@ -216,13 +212,10 @@ namespace osu.Game.Rulesets.UI KeyBindingInputManager = CreateInputManager(); KeyBindingInputManager.RelativeSizeAxes = Axes.Both; - - // Add mods, should always be the last thing applied to give full control to mods - applyMods(Mods); } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { KeyBindingInputManager.Add(content = new Container { @@ -235,6 +228,9 @@ namespace osu.Game.Rulesets.UI if (Cursor != null) KeyBindingInputManager.Add(Cursor); + // Apply mods + applyMods(Mods, config); + loadObjects(); } @@ -242,13 +238,16 @@ namespace osu.Game.Rulesets.UI /// Applies the active mods to this RulesetContainer. /// /// - private void applyMods(IEnumerable mods) + private void applyMods(IEnumerable mods, OsuConfigManager config) { if (mods == null) return; foreach (var mod in mods.OfType>()) mod.ApplyToRulesetContainer(this); + + foreach (var mod in mods.OfType()) + mod.ReadFromConfig(config); } public override void SetReplay(Replay replay) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 6f86d20295..830214803c 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// The step increase/decrease of the span of time visible by the length of the scrolling axes. /// - private const double time_span_step = 50; + private const double time_span_step = 200; /// /// The span of time that is visible by the length of the scrolling axes. @@ -88,10 +88,10 @@ namespace osu.Game.Rulesets.UI.Scrolling switch (args.Key) { case Key.Minus: - this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 200, Easing.OutQuint); + this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 600, Easing.OutQuint); break; case Key.Plus: - this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 200, Easing.OutQuint); + this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 600, Easing.OutQuint); break; } } diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs index 399f9274a6..caf9ba27ff 100644 --- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Edit.Components private const float corner_radius = 5; private const float contents_padding = 15; - public readonly Bindable Beatmap = new Bindable(); + protected readonly IBindable Beatmap = new Bindable(); protected Track Track => Beatmap.Value.Track; private readonly Drawable background; @@ -42,8 +42,9 @@ namespace osu.Game.Screens.Edit.Components } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(IBindableBeatmap beatmap, OsuColour colours) { + Beatmap.BindTo(beatmap); background.Colour = colours.Gray1; } } 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 c00e9ac4d5..07d9398d38 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.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 OpenTK; using osu.Framework.Configuration; using osu.Framework.Graphics; @@ -15,7 +16,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// public abstract class TimelinePart : CompositeDrawable { - public Bindable Beatmap = new Bindable(); + protected readonly IBindable Beatmap = new Bindable(); private readonly Container timeline; @@ -30,6 +31,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts }; } + [BackgroundDependencyLoader] + private void load(IBindableBeatmap beatmap) + { + Beatmap.BindTo(beatmap); + } + private void updateRelativeChildSize() { // the track may not be loaded completely (only has a length once it is). diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index 0301870588..77878288f9 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -20,19 +20,17 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary [BackgroundDependencyLoader] private void load(OsuColour colours, IAdjustableClock adjustableClock) { - TimelinePart markerPart, controlPointPart, bookmarkPart, breakPart; - Children = new Drawable[] { - markerPart = new MarkerPart(adjustableClock) { RelativeSizeAxes = Axes.Both }, - controlPointPart = new ControlPointPart + new MarkerPart(adjustableClock) { RelativeSizeAxes = Axes.Both }, + new ControlPointPart { Anchor = Anchor.Centre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Both, Height = 0.35f }, - bookmarkPart = new BookmarkPart + new BookmarkPart { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, @@ -67,7 +65,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary }, } }, - breakPart = new BreakPart + new BreakPart { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -75,11 +73,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary Height = 0.25f } }; - - markerPart.Beatmap.BindTo(Beatmap); - controlPointPart.Beatmap.BindTo(Beatmap); - bookmarkPart.Beatmap.BindTo(Beatmap); - breakPart.Beatmap.BindTo(Beatmap); } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index e4eaee76fc..2a2111c8d6 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -13,6 +13,7 @@ using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; +using osu.Framework.Platform; using osu.Framework.Timing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Screens; @@ -26,7 +27,7 @@ namespace osu.Game.Screens.Edit { protected override BackgroundScreen CreateBackground() => new BackgroundScreenCustom(@"Backgrounds/bg4"); - public override bool ShowOverlaysOnEnter => false; + protected override bool HideOverlaysOnEnter => true; public override bool AllowBeatmapRulesetChange => false; private Box bottomBackground; @@ -39,16 +40,19 @@ namespace osu.Game.Screens.Edit private EditorClock clock; private DependencyContainer dependencies; + private GameHost host; protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(parent); + => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, GameHost host) { + this.host = host; + // TODO: should probably be done at a RulesetContainer level to share logic with Player. var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); - clock = new EditorClock(Beatmap.Value.Beatmap.ControlPointInfo, beatDivisor) { IsCoupled = false }; + clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock.ChangeSource(sourceClock); dependencies.CacheAs(clock); @@ -128,9 +132,9 @@ namespace osu.Game.Screens.Edit { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 10 }, - Child = timeInfo = new TimeInfoContainer { RelativeSizeAxes = Axes.Both }, + Child = new TimeInfoContainer { RelativeSizeAxes = Axes.Both }, }, - timeline = new SummaryTimeline + new SummaryTimeline { RelativeSizeAxes = Axes.Both, }, @@ -138,7 +142,7 @@ namespace osu.Game.Screens.Edit { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = 10 }, - Child = playback = new PlaybackControl { RelativeSizeAxes = Axes.Both }, + Child = new PlaybackControl { RelativeSizeAxes = Axes.Both }, } }, } @@ -148,9 +152,6 @@ namespace osu.Game.Screens.Edit }, }; - timeInfo.Beatmap.BindTo(Beatmap); - timeline.Beatmap.BindTo(Beatmap); - playback.Beatmap.BindTo(Beatmap); menuBar.Mode.ValueChanged += onModeChanged; bottomBackground.Colour = colours.Gray2; @@ -158,7 +159,7 @@ namespace osu.Game.Screens.Edit private void exportBeatmap() { - Beatmap.Value.Save(); + host.OpenFileExternally(Beatmap.Value.Save()); } private void onModeChanged(EditorScreenMode mode) @@ -178,13 +179,12 @@ namespace osu.Game.Screens.Edit break; } - currentScreen.Beatmap.BindTo(Beatmap); LoadComponentAsync(currentScreen, screenContainer.Add); } - protected override bool OnWheel(InputState state) + protected override bool OnScroll(InputState state) { - if (state.Mouse.WheelDelta > 0) + if (state.Mouse.ScrollDelta.X + state.Mouse.ScrollDelta.Y > 0) clock.SeekBackward(true); else clock.SeekForward(true); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 67025f0620..1c40181ec9 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -5,8 +5,10 @@ using System; using System.Linq; using osu.Framework.MathUtils; using osu.Framework.Timing; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Screens.Edit.Screens.Compose; +using OpenTK; namespace osu.Game.Screens.Edit { @@ -15,15 +17,26 @@ namespace osu.Game.Screens.Edit /// public class EditorClock : DecoupleableInterpolatingFramedClock { + public readonly double TrackLength; + public ControlPointInfo ControlPointInfo; private readonly BindableBeatDivisor beatDivisor; - public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor) + public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) + { + this.beatDivisor = beatDivisor; + + ControlPointInfo = beatmap.Beatmap.ControlPointInfo; + TrackLength = beatmap.Track.Length; + } + + public EditorClock(ControlPointInfo controlPointInfo, double trackLength, BindableBeatDivisor beatDivisor) { this.beatDivisor = beatDivisor; ControlPointInfo = controlPointInfo; + TrackLength = trackLength; } /// @@ -111,6 +124,8 @@ namespace osu.Game.Screens.Edit if (seekTime > nextTimingPoint?.Time) seekTime = nextTimingPoint.Time; + // Ensure the sought point is within the boundaries + seekTime = MathHelper.Clamp(seekTime, 0, TrackLength); Seek(seekTime); } } diff --git a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs index fea4883144..a862485fd6 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs @@ -28,7 +28,6 @@ namespace osu.Game.Screens.Edit.Screens.Compose if (beatDivisor != null) this.beatDivisor.BindTo(beatDivisor); - ScrollableTimeline timeline; Children = new Drawable[] { new GridContainer @@ -65,7 +64,7 @@ namespace osu.Game.Screens.Edit.Screens.Compose { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 5 }, - Child = timeline = new ScrollableTimeline { RelativeSizeAxes = Axes.Both } + Child = new TimelineArea { RelativeSizeAxes = Axes.Both } }, new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } }, @@ -94,8 +93,6 @@ namespace osu.Game.Screens.Edit.Screens.Compose }, }; - timeline.Beatmap.BindTo(Beatmap); - var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance(); if (ruleset == null) { diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/BeatmapWaveformGraph.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/BeatmapWaveformGraph.cs deleted file mode 100644 index 72dda24b62..0000000000 --- a/osu.Game/Screens/Edit/Screens/Compose/Timeline/BeatmapWaveformGraph.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.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/CentreMarker.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/CentreMarker.cs new file mode 100644 index 0000000000..8e932f307d --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/CentreMarker.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using OpenTK; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class CentreMarker : CompositeDrawable + { + private const float triangle_width = 20; + private const float triangle_height = 10; + private const float bar_width = 2; + + public CentreMarker() + { + RelativeSizeAxes = Axes.Y; + Size = new Vector2(20, 1); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = bar_width, + }, + new Triangle + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(triangle_width, triangle_height), + Scale = new Vector2(1, -1) + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Red; + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollableTimeline.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollableTimeline.cs deleted file mode 100644 index 3223c08c1f..0000000000 --- a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollableTimeline.cs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using OpenTK; -using osu.Framework.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 deleted file mode 100644 index 83aa86ba61..0000000000 --- a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ScrollingTimelineContainer.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using OpenTK; -using osu.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/Timeline.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs new file mode 100644 index 0000000000..e993d36551 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/Timeline.cs @@ -0,0 +1,162 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Framework.Input; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Graphics; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class Timeline : ZoomableScrollContainer + { + public readonly Bindable WaveformVisible = new Bindable(); + public readonly IBindable Beatmap = new Bindable(); + + private IAdjustableClock adjustableClock; + + public Timeline() + { + ZoomDuration = 200; + ZoomEasing = Easing.OutQuint; + Zoom = 10; + ScrollbarVisible = false; + } + + private WaveformGraph waveform; + + [BackgroundDependencyLoader] + private void load(IBindableBeatmap beatmap, IAdjustableClock adjustableClock, OsuColour colours) + { + this.adjustableClock = adjustableClock; + + Child = waveform = new WaveformGraph + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Blue.Opacity(0.2f), + LowColour = colours.BlueLighter, + MidColour = colours.BlueDark, + HighColour = colours.BlueDarker, + Depth = float.MaxValue + }; + + // We don't want the centre marker to scroll + AddInternal(new CentreMarker()); + + WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint); + + Beatmap.BindTo(beatmap); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Beatmap.BindValueChanged(b => waveform.Waveform = b.Waveform); + waveform.Waveform = Beatmap.Value.Waveform; + } + + /// + /// The track's time in the previous frame. + /// + private double lastTrackTime; + + /// + /// Whether the user is currently dragging the timeline. + /// + private bool handlingDragInput; + + /// + /// Whether the track was playing before a user drag event. + /// + private bool trackWasPlaying; + + protected override void Update() + { + base.Update(); + + // The extrema of track time should be positioned at the centre of the container when scrolled to the start or end + Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 }; + + if (handlingDragInput) + { + // The user is dragging - the track should always follow the timeline + seekTrackToCurrent(); + } + else if (adjustableClock.IsRunning) + { + // If the user hasn't provided mouse input but the track is running, always follow the track + scrollToTrackTime(); + } + else + { + // The track isn't playing, so we want to smooth-scroll once more, and re-enable wheel scrolling + // There are two cases we have to be wary of: + // 1) The user scrolls on this timeline: We want the track to follow us + // 2) The user changes the track time through some other means (scrolling in the editor or overview timeline): We want to follow the track time + + // The simplest way to cover both cases is by checking that inter-frame track times are identical + if (adjustableClock.CurrentTime == lastTrackTime) + { + // The track hasn't been seeked externally + seekTrackToCurrent(); + } + else + { + // The track has been seeked externally + scrollToTrackTime(); + } + } + + lastTrackTime = adjustableClock.CurrentTime; + + void seekTrackToCurrent() + { + if (!(Beatmap.Value.Track is TrackVirtual)) + adjustableClock.Seek(Current / Content.DrawWidth * Beatmap.Value.Track.Length); + } + + void scrollToTrackTime() + { + if (!(Beatmap.Value.Track is TrackVirtual)) + ScrollTo((float)(adjustableClock.CurrentTime / Beatmap.Value.Track.Length) * Content.DrawWidth, false); + } + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (base.OnMouseDown(state, args)) + { + beginUserDrag(); + return true; + } + + return false; + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + endUserDrag(); + return base.OnMouseUp(state, args); + } + + private void beginUserDrag() + { + handlingDragInput = true; + trackWasPlaying = adjustableClock.IsRunning; + adjustableClock.Stop(); + } + + private void endUserDrag() + { + handlingDragInput = false; + if (trackWasPlaying) + adjustableClock.Start(); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/TimelineArea.cs new file mode 100644 index 0000000000..006317e57e --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/TimelineArea.cs @@ -0,0 +1,128 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class TimelineArea : CompositeDrawable + { + private readonly Timeline timeline; + + public TimelineArea() + { + Masking = true; + CornerRadius = 5; + + OsuCheckbox hitObjectsCheckbox; + OsuCheckbox hitSoundsCheckbox; + OsuCheckbox waveformCheckbox; + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("111") + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + 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 + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + 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 = () => timeline.Zoom++ + }, + new TimelineButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Y, + Height = 0.5f, + Icon = FontAwesome.fa_search_minus, + Action = () => timeline.Zoom-- + }, + } + } + } + }, + timeline = new Timeline { RelativeSizeAxes = Axes.Both } + }, + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Distributed), + } + } + }; + + hitObjectsCheckbox.Current.Value = true; + hitSoundsCheckbox.Current.Value = true; + waveformCheckbox.Current.Value = true; + + timeline.WaveformVisible.BindTo(waveformCheckbox.Current); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ZoomableScrollContainer.cs new file mode 100644 index 0000000000..0bfea68e50 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Timeline/ZoomableScrollContainer.cs @@ -0,0 +1,175 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Transforms; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using OpenTK; + +namespace osu.Game.Screens.Edit.Screens.Compose.Timeline +{ + public class ZoomableScrollContainer : ScrollContainer + { + /// + /// The time to zoom into/out of a point. + /// All user scroll input will be overwritten during the zoom transform. + /// + public double ZoomDuration; + + /// + /// The easing with which to transform the zoom. + /// + public Easing ZoomEasing; + + private readonly Container zoomedContent; + protected override Container Content => zoomedContent; + + private float currentZoom = 1; + + public ZoomableScrollContainer() + : base(Direction.Horizontal) + { + base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y }); + } + + private int minZoom = 1; + + /// + /// The minimum zoom level allowed. + /// + public int MinZoom + { + get => minZoom; + set + { + if (value < 1) + throw new ArgumentException($"{nameof(MinZoom)} must be >= 1.", nameof(value)); + minZoom = value; + + if (Zoom < value) + Zoom = value; + } + } + + private int maxZoom = 60; + + /// + /// The maximum zoom level allowed. + /// + public int MaxZoom + { + get => maxZoom; + set + { + if (value < 1) + throw new ArgumentException($"{nameof(MaxZoom)} must be >= 1.", nameof(value)); + maxZoom = value; + + if (Zoom > value) + Zoom = value; + } + } + + /// + /// Gets or sets the content zoom level of this . + /// + public float Zoom + { + get => zoomTarget; + set + { + value = MathHelper.Clamp(value, MinZoom, MaxZoom); + + if (IsLoaded) + setZoomTarget(value, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X); + else + currentZoom = zoomTarget = value; + } + } + + protected override void Update() + { + base.Update(); + + zoomedContent.Width = DrawWidth * currentZoom; + } + + protected override bool OnScroll(InputState state) + { + if (state.Mouse.HasPreciseScroll) + // for now, we don't support zoom when using a precision scroll device. this needs gesture support. + return base.OnScroll(state); + + setZoomTarget(zoomTarget + state.Mouse.ScrollDelta.Y, zoomedContent.ToLocalSpace(state.Mouse.NativeState.Position).X); + return true; + } + + private float zoomTarget = 1; + private void setZoomTarget(float newZoom, float focusPoint) + { + zoomTarget = MathHelper.Clamp(newZoom, MinZoom, MaxZoom); + transformZoomTo(zoomTarget, focusPoint, ZoomDuration, ZoomEasing); + } + + private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None) + => this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing)); + + private class TransformZoom : Transform + { + /// + /// The focus point in absolute coordinates local to the content. + /// + private readonly float focusPoint; + + /// + /// The size of the content. + /// + private readonly float contentSize; + + /// + /// The scroll offset at the start of the transform. + /// + private readonly float scrollOffset; + + /// + /// Transforms to a new value. + /// + /// The focus point in absolute coordinates local to the content. + /// The size of the content. + /// The scroll offset at the start of the transform. + public TransformZoom(float focusPoint, float contentSize, float scrollOffset) + { + this.focusPoint = focusPoint; + this.contentSize = contentSize; + this.scrollOffset = scrollOffset; + } + + public override string TargetMember => nameof(currentZoom); + + private float valueAt(double time) + { + if (time < StartTime) return StartValue; + if (time >= EndTime) return EndValue; + + return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); + } + + protected override void Apply(ZoomableScrollContainer d, double time) + { + float newZoom = valueAt(time); + + float focusOffset = focusPoint - scrollOffset; + float expectedWidth = d.DrawWidth * newZoom; + float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset; + + d.currentZoom = newZoom; + d.ScrollTo(targetOffset, false); + } + + protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.currentZoom; + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/EditorScreen.cs b/osu.Game/Screens/Edit/Screens/EditorScreen.cs index f70c462cd8..f8402b9a9f 100644 --- a/osu.Game/Screens/Edit/Screens/EditorScreen.cs +++ b/osu.Game/Screens/Edit/Screens/EditorScreen.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,7 +14,7 @@ namespace osu.Game.Screens.Edit.Screens /// public class EditorScreen : Container { - public readonly Bindable Beatmap = new Bindable(); + protected readonly IBindable Beatmap = new Bindable(); protected override Container Content => content; private readonly Container content; @@ -27,6 +28,12 @@ namespace osu.Game.Screens.Edit.Screens InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; } + [BackgroundDependencyLoader] + private void load(IBindableBeatmap beatmap) + { + Beatmap.BindTo(beatmap); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 555c497d92..c3b3e747fd 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -17,7 +16,7 @@ namespace osu.Game.Screens { private bool showDisclaimer; - public override bool ShowOverlaysOnEnter => false; + protected override bool HideOverlaysOnEnter => true; protected override bool AllowBackButton => false; @@ -30,43 +29,48 @@ namespace osu.Game.Screens { base.LogoArriving(logo, resuming); + logo.BeatMatching = false; 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); + logo.Delay(500).FadeInFromZero(1000, Easing.OutQuint); } protected override void LogoSuspending(OsuLogo logo) { base.LogoSuspending(logo); - logo.FadeOut(100); + logo.FadeOut(logo.Alpha * 400); + } + + private OsuScreen loadableScreen; + private ShaderPrecompiler precompiler; + + protected virtual OsuScreen CreateLoadableScreen() => showDisclaimer ? (OsuScreen)new Disclaimer() : new Intro(); + + protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler(); + + protected override void OnEntering(Screen last) + { + base.OnEntering(last); + + LoadComponentAsync(precompiler = CreateShaderPrecompiler(), Add); + LoadComponentAsync(loadableScreen = CreateLoadableScreen()); + + checkIfLoaded(); + } + + private void checkIfLoaded() + { + if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling) + { + Schedule(checkIfLoaded); + return; + } + + Push(loadableScreen); } [BackgroundDependencyLoader] @@ -80,16 +84,10 @@ namespace osu.Game.Screens /// 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) { @@ -103,16 +101,17 @@ namespace osu.Game.Screens loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); } + protected virtual bool AllLoaded => loadTargets.All(s => s.Loaded); + protected override void Update() { base.Update(); // if our target is null we are done. - if (loadTargets.All(s => s.Loaded)) + if (AllLoaded) { FinishedCompiling = true; Expire(); - onLoaded?.Invoke(); } } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 997002327a..81abc4cd3d 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -8,7 +8,6 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -17,6 +16,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Input.Bindings; +using osu.Game.Overlays; using OpenTK; using OpenTK.Graphics; using OpenTK.Input; @@ -27,8 +27,6 @@ namespace osu.Game.Screens.Menu { public event Action StateChanged; - private readonly BindableBool showOverlays = new BindableBool(); - public Action OnEdit; public Action OnExit; public Action OnDirect; @@ -132,10 +130,12 @@ namespace osu.Game.Screens.Menu buttonFlow.AddRange(buttonsTopLevel); } + private OsuGame game; + [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuGame game) { - if (game != null) showOverlays.BindTo(game.ShowOverlays); + this.game = game; sampleBack = audio.Sample.Get(@"Menu/button-back-select"); } @@ -148,6 +148,8 @@ namespace osu.Game.Screens.Menu case Key.Space: logo?.TriggerOnClick(state); return true; + case Key.Escape: + return goBack(); } return false; @@ -158,17 +160,22 @@ namespace osu.Game.Screens.Menu switch (action) { case GlobalAction.Back: - switch (State) - { - case MenuState.TopLevel: - State = MenuState.Initial; - return true; - case MenuState.Play: - backButton.TriggerOnClick(); - return true; - default: - return false; - } + return goBack(); + default: + return false; + } + } + + private bool goBack() + { + switch (State) + { + case MenuState.TopLevel: + State = MenuState.Initial; + return true; + case MenuState.Play: + backButton.TriggerOnClick(); + return true; default: return false; } @@ -241,9 +248,6 @@ namespace osu.Game.Screens.Menu backButton.ContractStyle = 0; settingsButton.ContractStyle = 0; - if (state == MenuState.TopLevel) - buttonArea.FinishTransforms(true); - updateLogoState(lastState); using (buttonArea.BeginDelayedSequence(lastState == MenuState.Initial ? 150 : 0, true)) @@ -312,49 +316,63 @@ namespace osu.Game.Screens.Menu { if (logo == null) return; - logoDelayedAction?.Cancel(); - switch (state) { case MenuState.Exit: case MenuState.Initial: - logoTracking = false; - + logoDelayedAction?.Cancel(); logoDelayedAction = Scheduler.AddDelayed(() => - { - showOverlays.Value = false; + { + logoTracking = false; - logo.ClearTransforms(targetMember: nameof(Position)); - logo.RelativePositionAxes = Axes.Both; + if (game != null) + { + game.OverlayActivationMode.Value = state == MenuState.Exit ? OverlayActivation.Disabled : OverlayActivation.All; + game.Toolbar.Hide(); + } - logo.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo); - logo.ScaleTo(1, 800, Easing.OutExpo); - }, 150); + logo.ClearTransforms(targetMember: nameof(Position)); + logo.RelativePositionAxes = Axes.Both; + logo.MoveTo(new Vector2(0.5f), 800, Easing.OutExpo); + logo.ScaleTo(1, 800, Easing.OutExpo); + }, buttonArea.Alpha * 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 + break; case MenuState.Initial: - logoTracking = false; - logo.ScaleTo(0.5f, 200, Easing.In); + logo.ClearTransforms(targetMember: nameof(Position)); + logo.RelativePositionAxes = Axes.None; + + bool impact = logo.Scale.X > 0.6f; + + if (lastState == MenuState.Initial) + logo.ScaleTo(0.5f, 200, Easing.In); logo.MoveTo(logoTrackingPosition, lastState == MenuState.EnteringMode ? 0 : 200, Easing.In); + logoDelayedAction?.Cancel(); logoDelayedAction = Scheduler.AddDelayed(() => { logoTracking = true; - logo.Impact(); - showOverlays.Value = true; + if (impact) + logo.Impact(); + + if (game != null) + { + game.OverlayActivationMode.Value = OverlayActivation.All; + game.Toolbar.State = Visibility.Visible; + } }, 200); break; default: + logo.ClearTransforms(targetMember: nameof(Position)); + logo.RelativePositionAxes = Axes.None; logoTracking = true; logo.ScaleTo(0.5f, 200, Easing.OutQuint); break; diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 5af634b02d..0c70dbf570 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -9,6 +9,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; +using osu.Game.Overlays; namespace osu.Game.Screens.Menu { @@ -18,7 +19,9 @@ namespace osu.Game.Screens.Menu private readonly SpriteIcon icon; private Color4 iconColour; - public override bool ShowOverlaysOnEnter => false; + protected override bool HideOverlaysOnEnter => true; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; + public override bool CursorVisible => false; public Disclaimer() diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs index 4de76e530a..c1032011f7 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/Intro.cs @@ -15,6 +15,7 @@ using osu.Game.IO.Archives; using osu.Game.Screens.Backgrounds; using OpenTK; using OpenTK.Graphics; +using osu.Game.Overlays; namespace osu.Game.Screens.Menu { @@ -27,11 +28,15 @@ namespace osu.Game.Screens.Menu /// public bool DidLoadMenu; + private readonly Bindable beatmap = new Bindable(); + private MainMenu mainMenu; private SampleChannel welcome; private SampleChannel seeya; - public override bool ShowOverlaysOnEnter => false; + protected override bool HideOverlaysOnEnter => true; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; + public override bool CursorVisible => false; protected override BackgroundScreen CreateBackground() => new BackgroundScreenEmpty(); @@ -39,11 +44,13 @@ namespace osu.Game.Screens.Menu private Bindable menuVoice; private Bindable menuMusic; private Track track; - private WorkingBeatmap beatmap; + private WorkingBeatmap introBeatmap; [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuConfigManager config, BeatmapManager beatmaps, Framework.Game game) + private void load(AudioManager audio, OsuConfigManager config, BeatmapManager beatmaps, Framework.Game game, BindableBeatmap beatmap) { + this.beatmap.BindTo(beatmap); + menuVoice = config.GetBindable(OsuSetting.MenuVoice); menuMusic = config.GetBindable(OsuSetting.MenuMusic); @@ -70,38 +77,13 @@ namespace osu.Game.Screens.Menu } } - beatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - track = beatmap.Track; + introBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); + track = introBeatmap.Track; welcome = audio.Sample.Get(@"welcome"); seeya = audio.Sample.Get(@"seeya"); } - protected override void OnEntering(Screen last) - { - base.OnEntering(last); - - Game.Beatmap.Value = beatmap; - - if (menuVoice) - welcome.Play(); - - Scheduler.AddDelayed(delegate - { - // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Manu. - if (menuMusic) - track.Start(); - - LoadComponentAsync(mainMenu = new MainMenu()); - - Scheduler.AddDelayed(delegate - { - DidLoadMenu = true; - Push(mainMenu); - }, delay_step_one); - }, delay_step_two); - } - private const double delay_step_one = 2300; private const double delay_step_two = 600; @@ -111,6 +93,29 @@ namespace osu.Game.Screens.Menu { base.LogoArriving(logo, resuming); + if (!resuming) + { + beatmap.Value = introBeatmap; + + if (menuVoice) + welcome.Play(); + + Scheduler.AddDelayed(delegate + { + // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Manu. + if (menuMusic) + track.Start(); + + LoadComponentAsync(mainMenu = new MainMenu()); + + Scheduler.AddDelayed(delegate + { + DidLoadMenu = true; + Push(mainMenu); + }, delay_step_one); + }, delay_step_two); + } + logo.RelativePositionAxes = Axes.Both; logo.Colour = Color4.White; logo.Ripple = false; diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 1f2cb915b3..fb6130fa36 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Menu { public class LogoVisualisation : Drawable, IHasAccentColour { - private readonly Bindable beatmap = new Bindable(); + private readonly IBindable beatmap = new Bindable(); /// /// The number of bars to jump each update iteration. @@ -78,9 +78,9 @@ namespace osu.Game.Screens.Menu } [BackgroundDependencyLoader] - private void load(ShaderManager shaders, OsuGameBase game) + private void load(ShaderManager shaders, IBindableBeatmap beatmap) { - beatmap.BindTo(game.Beatmap); + this.beatmap.BindTo(beatmap); shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 3b519c5259..cbdd8b4e8b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Menu { private readonly ButtonSystem buttons; - public override bool ShowOverlaysOnEnter => buttons.State != MenuState.Initial; + protected override bool HideOverlaysOnEnter => buttons.State == MenuState.Initial; protected override bool AllowBackButton => buttons.State != MenuState.Initial; diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index fae3e72552..7d46aad089 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -22,10 +22,10 @@ namespace osu.Game.Screens.Menu public override bool HandleKeyboardInput => false; public override bool HandleMouseInput => false; - private readonly Bindable beatmap = new Bindable(); + private readonly IBindable beatmap = new Bindable(); - private readonly Box leftBox; - private readonly Box rightBox; + private Box leftBox; + private Box rightBox; private const float amplitude_dead_zone = 0.25f; private const float alpha_multiplier = (1 - amplitude_dead_zone) / 0.55f; @@ -42,6 +42,17 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both; Anchor = Anchor.Centre; Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(IBindableBeatmap beatmap, OsuColour colours) + { + this.beatmap.BindTo(beatmap); + + // linear colour looks better in this case, so let's use it for now. + Color4 gradientDark = colours.Blue.Opacity(0).ToLinear(); + Color4 gradientLight = colours.Blue.Opacity(0.6f).ToLinear(); + Children = new Drawable[] { leftBox = new Box @@ -49,35 +60,27 @@ namespace osu.Game.Screens.Menu Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Y, - Width = box_width, + Width = box_width * 2, + // align off-screen to make sure our edges don't become visible during parallax. + X = -box_width, Alpha = 0, Blending = BlendingMode.Additive, + Colour = ColourInfo.GradientHorizontal(gradientLight, gradientDark) }, rightBox = new Box { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Y, - Width = box_width, + Width = box_width * 2, + X = box_width, Alpha = 0, Blending = BlendingMode.Additive, + Colour = ColourInfo.GradientHorizontal(gradientDark, gradientLight) } }; } - [BackgroundDependencyLoader] - private void load(OsuGameBase game, OsuColour colours) - { - beatmap.BindTo(game.Beatmap); - - // linear colour looks better in this case, so let's use it for now. - Color4 gradientDark = colours.Blue.Opacity(0).ToLinear(); - Color4 gradientLight = colours.Blue.Opacity(0.3f).ToLinear(); - - leftBox.Colour = ColourInfo.GradientHorizontal(gradientLight, gradientDark); - rightBox.Colour = ColourInfo.GradientHorizontal(gradientDark, gradientLight); - } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) { if (beatIndex < 0) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 42a8dbd5da..16482b0e48 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -64,6 +64,8 @@ namespace osu.Game.Screens.Menu set { colourAndTriangles.FadeTo(value ? 1 : 0, transition_length, Easing.OutQuint); } } + public bool BeatMatching = true; + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => logoContainer.ReceiveMouseInputAt(screenSpacePos); public bool Ripple @@ -264,6 +266,8 @@ namespace osu.Game.Screens.Menu { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + if (!BeatMatching) return; + lastBeatIndex = beatIndex; var beatLength = timingPoint.BeatLength; diff --git a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs new file mode 100644 index 0000000000..42863754c5 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/BeatmapTitle.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Multi.Components +{ + public class BeatmapTitle : FillFlowContainer + { + private readonly OsuSpriteText beatmapTitle, beatmapDash, beatmapArtist; + + private LocalisationEngine localisation; + + public float TextSize + { + set { beatmapTitle.TextSize = beatmapDash.TextSize = beatmapArtist.TextSize = value; } + } + + private BeatmapInfo beatmap; + + public BeatmapInfo Beatmap + { + set + { + if (value == beatmap) return; + beatmap = value; + + if (IsLoaded) + updateText(); + } + } + + public BeatmapTitle() + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Horizontal; + + Children = new[] + { + beatmapTitle = new OsuSpriteText { Font = @"Exo2.0-BoldItalic", }, + beatmapDash = new OsuSpriteText { Font = @"Exo2.0-BoldItalic", }, + beatmapArtist = new OsuSpriteText { Font = @"Exo2.0-RegularItalic", }, + }; + } + + [BackgroundDependencyLoader] + private void load(LocalisationEngine localisation) + { + this.localisation = localisation; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateText(); + } + + private void updateText() + { + if (beatmap == null) + { + beatmapTitle.Current = beatmapArtist.Current = null; + beatmapTitle.Text = "Changing map"; + beatmapDash.Text = beatmapArtist.Text = string.Empty; + } + else + { + beatmapTitle.Current = localisation.GetUnicodePreference(beatmap.Metadata.TitleUnicode, beatmap.Metadata.Title); + beatmapDash.Text = @" - "; + beatmapArtist.Current = localisation.GetUnicodePreference(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist); + } + } + } +} diff --git a/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs b/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs new file mode 100644 index 0000000000..78ffe01ef0 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Multiplayer; +using OpenTK; + +namespace osu.Game.Screens.Multi.Components +{ + public class BeatmapTypeInfo : FillFlowContainer + { + private readonly ModeTypeInfo modeTypeInfo; + private readonly BeatmapTitle beatmapTitle; + private readonly OsuSpriteText beatmapAuthor; + + public BeatmapInfo Beatmap + { + set + { + modeTypeInfo.Beatmap = beatmapTitle.Beatmap = value; + beatmapAuthor.Text = value == null ? string.Empty : $"mapped by {value.Metadata.Author}"; + } + } + + public GameType Type + { + set { modeTypeInfo.Type = value; } + } + + public BeatmapTypeInfo() + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Horizontal; + LayoutDuration = 100; + Spacing = new Vector2(5f, 0f); + + Children = new Drawable[] + { + modeTypeInfo = new ModeTypeInfo(), + new Container + { + AutoSizeAxes = Axes.X, + Height = 30, + Margin = new MarginPadding { Left = 5 }, + Children = new Drawable[] + { + beatmapTitle = new BeatmapTitle(), + beatmapAuthor = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + TextSize = 14, + }, + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + beatmapAuthor.Colour = colours.Gray9; + } + } +} diff --git a/osu.Game/Screens/Multi/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Components/DrawableRoom.cs index 040fbaf593..54bd0ae7cc 100644 --- a/osu.Game/Screens/Multi/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Components/DrawableRoom.cs @@ -1,18 +1,22 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; +using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Users; using OpenTK; @@ -20,158 +24,213 @@ using OpenTK.Graphics; namespace osu.Game.Screens.Multi.Components { - public class DrawableRoom : OsuClickableContainer + public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable { - private const float transition_duration = 100; + public const float SELECTION_BORDER_WIDTH = 4; + private const float corner_radius = 5; + private const float transition_duration = 60; private const float content_padding = 10; private const float height = 100; private const float side_strip_width = 5; private const float cover_width = 145; - private readonly Box sideStrip; - private readonly Container coverContainer; - private readonly OsuSpriteText name, status, beatmapTitle, beatmapDash, beatmapArtist; - private readonly FillFlowContainer beatmapInfoFlow; - private readonly ParticipantInfo participantInfo; - private readonly ModeTypeInfo modeTypeInfo; + private readonly Box selectionBox; private readonly Bindable nameBind = new Bindable(); private readonly Bindable hostBind = new Bindable(); private readonly Bindable statusBind = new Bindable(); private readonly Bindable typeBind = new Bindable(); private readonly Bindable beatmapBind = new Bindable(); - private readonly Bindable participantsBind = new Bindable(); - - private OsuColour colours; - private LocalisationEngine localisation; + private readonly Bindable> participantsBind = new Bindable>(); public readonly Room Room; + private SelectionState state; + public SelectionState State + { + get { return state; } + set + { + if (value == state) return; + state = value; + + if (state == SelectionState.Selected) + selectionBox.FadeIn(transition_duration); + else + selectionBox.FadeOut(transition_duration); + + StateChanged?.Invoke(State); + } + } + + public IEnumerable FilterTerms => new[] { Room.Name.Value }; + + private bool matchingFilter; + public bool MatchingFilter + { + get { return matchingFilter; } + set + { + matchingFilter = value; + this.FadeTo(MatchingFilter ? 1 : 0, 200); + } + } + + private Action action; + public new Action Action + { + get { return action; } + set + { + action = value; + Enabled.Value = action != null; + } + } + + public event Action StateChanged; + public DrawableRoom(Room room) { Room = room; RelativeSizeAxes = Axes.X; - Height = height; - CornerRadius = 5; + Height = height + SELECTION_BORDER_WIDTH * 2; + CornerRadius = corner_radius + SELECTION_BORDER_WIDTH / 2; Masking = true; - EdgeEffect = new EdgeEffectParameters + + // create selectionBox here so State can be set before being loaded + selectionBox = new Box { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(40), - Radius = 5, + RelativeSizeAxes = Axes.Both, + Alpha = 0f, }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Box sideStrip; + UpdateableBeatmapSetCover cover; + OsuSpriteText name, status; + ParticipantInfo participantInfo; + BeatmapTitle beatmapTitle; + ModeTypeInfo modeTypeInfo; Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"212121"), - }, - sideStrip = new Box - { - RelativeSizeAxes = Axes.Y, - Width = side_strip_width, - }, - new Container - { - Width = cover_width, - RelativeSizeAxes = Axes.Y, - Masking = true, - Margin = new MarginPadding { Left = side_strip_width }, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - coverContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - }, - }, + selectionBox, new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + Padding = new MarginPadding(SELECTION_BORDER_WIDTH), + Child = new Container { - Vertical = content_padding, - Left = side_strip_width + cover_width + content_padding, - Right = content_padding, - }, - Children = new Drawable[] - { - new FillFlowContainer + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + EdgeEffect = new EdgeEffectParameters { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5f), - Children = new Drawable[] + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(40), + Radius = 5, + }, + Children = new Drawable[] + { + new Box { - name = new OsuSpriteText - { - TextSize = 18, - }, - participantInfo = new ParticipantInfo(), + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"212121"), }, - }, - new FillFlowContainer - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + sideStrip = new Box { - status = new OsuSpriteText + RelativeSizeAxes = Axes.Y, + Width = side_strip_width, + }, + cover = new UpdateableBeatmapSetCover + { + Width = cover_width, + RelativeSizeAxes = Axes.Y, + Masking = true, + Margin = new MarginPadding { Left = side_strip_width }, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { - TextSize = 14, - Font = @"Exo2.0-Bold", + Vertical = content_padding, + Left = side_strip_width + cover_width + content_padding, + Right = content_padding, }, - beatmapInfoFlow = new FillFlowContainer + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new[] + new FillFlowContainer { - beatmapTitle = new OsuSpriteText + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5f), + Children = new Drawable[] { - TextSize = 14, - Font = @"Exo2.0-BoldItalic", + name = new OsuSpriteText + { + TextSize = 18, + }, + participantInfo = new ParticipantInfo(), }, - beatmapDash = new OsuSpriteText + }, + new FillFlowContainer + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - TextSize = 14, - Font = @"Exo2.0-BoldItalic", - }, - beatmapArtist = new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-RegularItalic", + status = new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-Bold", + }, + beatmapTitle = new BeatmapTitle + { + TextSize = 14, + Colour = colours.Gray9 + }, }, }, + modeTypeInfo = new ModeTypeInfo + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, }, }, }, - modeTypeInfo = new ModeTypeInfo - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }, }, }, }; - nameBind.ValueChanged += displayName; - hostBind.ValueChanged += displayUser; - typeBind.ValueChanged += displayGameType; - participantsBind.ValueChanged += displayParticipants; + nameBind.ValueChanged += n => name.Text = n; + hostBind.ValueChanged += h => participantInfo.Host = h; + typeBind.ValueChanged += m => modeTypeInfo.Type = m; + participantsBind.ValueChanged += p => participantInfo.Participants = p; + + statusBind.ValueChanged += s => + { + status.Text = s.Message; + + foreach (Drawable d in new Drawable[] { selectionBox, sideStrip, status }) + d.FadeColour(s.GetAppropriateColour(colours), transition_duration); + }; + + beatmapBind.ValueChanged += b => + { + cover.BeatmapSet = b?.BeatmapSet; + beatmapTitle.Beatmap = b; + modeTypeInfo.Beatmap = b; + }; nameBind.BindTo(Room.Name); hostBind.BindTo(Room.Host); @@ -181,82 +240,21 @@ namespace osu.Game.Screens.Multi.Components participantsBind.BindTo(Room.Participants); } - [BackgroundDependencyLoader] - private void load(OsuColour colours, LocalisationEngine localisation) + protected override void LoadComplete() { - this.localisation = localisation; - this.colours = colours; - - beatmapInfoFlow.Colour = colours.Gray9; - - //binded here instead of ctor because dependencies are needed - statusBind.ValueChanged += displayStatus; - beatmapBind.ValueChanged += displayBeatmap; - - statusBind.TriggerChange(); - beatmapBind.TriggerChange(); + base.LoadComplete(); + this.FadeInFromZero(transition_duration); } - private void displayName(string value) + protected override bool OnClick(InputState state) { - name.Text = value; - } - - private void displayUser(User value) - { - participantInfo.Host = value; - } - - private void displayStatus(RoomStatus value) - { - if (value == null) return; - status.Text = value.Message; - - foreach (Drawable d in new Drawable[] { sideStrip, status }) - d.FadeColour(value.GetAppropriateColour(colours), 100); - } - - private void displayGameType(GameType value) - { - modeTypeInfo.Type = value; - } - - private void displayBeatmap(BeatmapInfo value) - { - modeTypeInfo.Beatmap = value; - - if (value != null) + if (Enabled.Value) { - coverContainer.FadeIn(transition_duration); - - LoadComponentAsync(new BeatmapSetCover(value.BeatmapSet) - { - 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 = @" - "; - beatmapArtist.Current = localisation.GetUnicodePreference(value.Metadata.ArtistUnicode, value.Metadata.Artist); + Action?.Invoke(this); + State = SelectionState.Selected; } - else - { - coverContainer.FadeOut(transition_duration); - beatmapTitle.Current = null; - beatmapArtist.Current = null; - - beatmapTitle.Text = "Changing map"; - beatmapDash.Text = beatmapArtist.Text = string.Empty; - } - } - - private void displayParticipants(User[] value) - { - participantInfo.Participants = value; + return true; } } } diff --git a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs index e3aba685a7..e2d0268090 100644 --- a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs @@ -64,6 +64,7 @@ namespace osu.Game.Screens.Multi.Components AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(5f, 0f), + LayoutDuration = 100, Children = new[] { rulesetContainer = new Container diff --git a/osu.Game/Screens/Multi/Components/ParticipantCount.cs b/osu.Game/Screens/Multi/Components/ParticipantCount.cs new file mode 100644 index 0000000000..e7183cbd92 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/ParticipantCount.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Multi.Components +{ + public class ParticipantCount : FillFlowContainer + { + private const float text_size = 30; + private const float transition_duration = 100; + + private readonly OsuSpriteText count, slash, maxText; + + public int Count + { + set => count.Text = value.ToString(); + } + + private int? max; + public int? Max + { + get => max; + set + { + if (value == max) return; + max = value; + + updateMax(); + } + } + + public ParticipantCount() + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Horizontal; + LayoutDuration = transition_duration; + + Children = new[] + { + count = new OsuSpriteText + { + TextSize = text_size, + Font = @"Exo2.0-Bold" + }, + slash = new OsuSpriteText + { + Text = @"/", + TextSize = text_size, + Font = @"Exo2.0-Light" + }, + maxText = new OsuSpriteText + { + TextSize = text_size, + Font = @"Exo2.0-Light" + }, + }; + + updateMax(); + } + + private void updateMax() + { + if (Max == null) + { + slash.FadeOut(transition_duration); + maxText.FadeOut(transition_duration); + } + else + { + slash.FadeIn(transition_duration); + maxText.Text = Max.ToString(); + maxText.FadeIn(transition_duration); + } + } + } +} diff --git a/osu.Game/Screens/Multi/Components/RoomInspector.cs b/osu.Game/Screens/Multi/Components/RoomInspector.cs index 92910e8301..22bca1efc7 100644 --- a/osu.Game/Screens/Multi/Components/RoomInspector.cs +++ b/osu.Game/Screens/Multi/Components/RoomInspector.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -10,7 +11,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -25,30 +25,28 @@ namespace osu.Game.Screens.Multi.Components { public class RoomInspector : Container { - private readonly MarginPadding contentPadding = new MarginPadding { Horizontal = 20, Vertical = 10 }; private const float transition_duration = 100; - private readonly Box statusStrip; - private readonly Container coverContainer; - private readonly FillFlowContainer topFlow, participantsFlow; - private readonly ModeTypeInfo modeTypeInfo; - private readonly OsuSpriteText participants, participantsSlash, maxParticipants, name, status, beatmapTitle, beatmapDash, beatmapArtist, beatmapAuthor; - private readonly ParticipantInfo participantInfo; - private readonly ScrollContainer participantsScroll; - + private readonly MarginPadding contentPadding = new MarginPadding { Horizontal = 20, Vertical = 10 }; private readonly Bindable nameBind = new Bindable(); private readonly Bindable hostBind = new Bindable(); private readonly Bindable statusBind = new Bindable(); private readonly Bindable typeBind = new Bindable(); private readonly Bindable beatmapBind = new Bindable(); private readonly Bindable maxParticipantsBind = new Bindable(); - private readonly Bindable participantsBind = new Bindable(); + private readonly Bindable> participantsBind = new Bindable>(); private OsuColour colours; - private LocalisationEngine localisation; + private Box statusStrip; + private UpdateableBeatmapSetCover cover; + private ParticipantCount participantCount; + private FillFlowContainer topFlow, participantsFlow; + private OsuSpriteText name, status; + private BeatmapTypeInfo beatmapTypeInfo; + private ScrollContainer participantsScroll; + private ParticipantInfo participantInfo; private Room room; - public Room Room { get { return room; } @@ -57,20 +55,33 @@ namespace osu.Game.Screens.Multi.Components if (value == room) return; room = value; - nameBind.BindTo(Room.Name); - hostBind.BindTo(Room.Host); - statusBind.BindTo(Room.Status); - typeBind.BindTo(Room.Type); - beatmapBind.BindTo(Room.Beatmap); - maxParticipantsBind.BindTo(Room.MaxParticipants); - participantsBind.BindTo(Room.Participants); + nameBind.UnbindBindings(); + hostBind.UnbindBindings(); + statusBind.UnbindBindings(); + typeBind.UnbindBindings(); + beatmapBind.UnbindBindings(); + maxParticipantsBind.UnbindBindings(); + participantsBind.UnbindBindings(); + + if (room != null) + { + nameBind.BindTo(room.Name); + hostBind.BindTo(room.Host); + statusBind.BindTo(room.Status); + typeBind.BindTo(room.Type); + beatmapBind.BindTo(room.Beatmap); + maxParticipantsBind.BindTo(room.MaxParticipants); + participantsBind.BindTo(room.Participants); + } + + updateState(); } } - public RoomInspector() + [BackgroundDependencyLoader] + private void load(OsuColour colours) { - Width = 520; - RelativeSizeAxes = Axes.Y; + this.colours = colours; Children = new Drawable[] { @@ -93,21 +104,9 @@ namespace osu.Game.Screens.Multi.Components Masking = true, Children = new Drawable[] { - new Container + cover = new UpdateableBeatmapSetCover { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - coverContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - }, }, new Box { @@ -120,32 +119,10 @@ namespace osu.Game.Screens.Multi.Components Padding = new MarginPadding(20), Children = new Drawable[] { - new FillFlowContainer + participantCount = new ParticipantCount { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - LayoutDuration = transition_duration, - Children = new[] - { - participants = new OsuSpriteText - { - TextSize = 30, - Font = @"Exo2.0-Bold" - }, - participantsSlash = new OsuSpriteText - { - Text = @"/", - TextSize = 30, - Font = @"Exo2.0-Light" - }, - maxParticipants = new OsuSpriteText - { - TextSize = 30, - Font = @"Exo2.0-Light" - }, - }, }, name = new OsuSpriteText { @@ -178,6 +155,7 @@ namespace osu.Game.Screens.Multi.Components RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, + LayoutDuration = transition_duration, Padding = contentPadding, Spacing = new Vector2(0f, 5f), Children = new Drawable[] @@ -187,53 +165,7 @@ namespace osu.Game.Screens.Multi.Components TextSize = 14, Font = @"Exo2.0-Bold", }, - new FillFlowContainer - { - AutoSizeAxes = Axes.X, - Height = 30, - Direction = FillDirection.Horizontal, - LayoutDuration = transition_duration, - Spacing = new Vector2(5f, 0f), - Children = new Drawable[] - { - modeTypeInfo = new ModeTypeInfo(), - new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Margin = new MarginPadding { Left = 5 }, - Children = new[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new[] - { - beatmapTitle = new OsuSpriteText - { - Font = @"Exo2.0-BoldItalic", - }, - beatmapDash = new OsuSpriteText - { - Font = @"Exo2.0-BoldItalic", - }, - beatmapArtist = new OsuSpriteText - { - Font = @"Exo2.0-RegularItalic", - }, - }, - }, - beatmapAuthor = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - TextSize = 14, - }, - }, - }, - }, - }, + beatmapTypeInfo = new BeatmapTypeInfo(), }, }, }, @@ -269,27 +201,26 @@ namespace osu.Game.Screens.Multi.Components }, }; - nameBind.ValueChanged += displayName; - hostBind.ValueChanged += displayUser; - typeBind.ValueChanged += displayGameType; - maxParticipantsBind.ValueChanged += displayMaxParticipants; - participantsBind.ValueChanged += displayParticipants; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, LocalisationEngine localisation) - { - this.localisation = localisation; - this.colours = colours; - - beatmapAuthor.Colour = colours.Gray9; - - //binded here instead of ctor because dependencies are needed + nameBind.ValueChanged += n => name.Text = n; + hostBind.ValueChanged += h => participantInfo.Host = h; + typeBind.ValueChanged += t => beatmapTypeInfo.Type = t; + maxParticipantsBind.ValueChanged += m => participantCount.Max = m; statusBind.ValueChanged += displayStatus; - beatmapBind.ValueChanged += displayBeatmap; - statusBind.TriggerChange(); - beatmapBind.TriggerChange(); + beatmapBind.ValueChanged += b => + { + cover.BeatmapSet = b?.BeatmapSet; + beatmapTypeInfo.Beatmap = b; + }; + + participantsBind.ValueChanged += p => + { + participantCount.Count = p.Count(); + participantInfo.Participants = p; + participantsFlow.ChildrenEnumerable = p.Select(u => new UserTile(u)); + }; + + updateState(); } protected override void UpdateAfterChildren() @@ -299,86 +230,41 @@ namespace osu.Game.Screens.Multi.Components participantsScroll.Height = DrawHeight - topFlow.DrawHeight; } - private void displayName(string value) + private void displayStatus(RoomStatus s) { - name.Text = value; + status.Text = s.Message; + + Color4 c = s.GetAppropriateColour(colours); + statusStrip.FadeColour(c, transition_duration); + status.FadeColour(c, transition_duration); } - private void displayUser(User value) + private void updateState() { - participantInfo.Host = value; - } - - private void displayStatus(RoomStatus value) - { - status.Text = value.Message; - - foreach (Drawable d in new Drawable[] { statusStrip, status }) - d.FadeColour(value.GetAppropriateColour(colours), transition_duration); - } - - private void displayGameType(GameType value) - { - modeTypeInfo.Type = value; - } - - private void displayBeatmap(BeatmapInfo value) - { - modeTypeInfo.Beatmap = value; - - if (value != null) + if (Room == null) { - coverContainer.FadeIn(transition_duration); + cover.BeatmapSet = null; + participantsFlow.FadeOut(transition_duration); + participantCount.FadeOut(transition_duration); + beatmapTypeInfo.FadeOut(transition_duration); + name.FadeOut(transition_duration); + participantInfo.FadeOut(transition_duration); - LoadComponentAsync(new BeatmapSetCover(value.BeatmapSet) - { - 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 = @" - "; - beatmapArtist.Current = localisation.GetUnicodePreference(value.Metadata.ArtistUnicode, value.Metadata.Artist); - beatmapAuthor.Text = $"mapped by {value.Metadata.Author}"; + displayStatus(new RoomStatusNoneSelected()); } else { - coverContainer.FadeOut(transition_duration); + participantsFlow.FadeIn(transition_duration); + participantCount.FadeIn(transition_duration); + beatmapTypeInfo.FadeIn(transition_duration); + name.FadeIn(transition_duration); + participantInfo.FadeIn(transition_duration); - beatmapTitle.Current = null; - beatmapArtist.Current = null; - - beatmapTitle.Text = "Changing map"; - beatmapDash.Text = beatmapArtist.Text = beatmapAuthor.Text = string.Empty; + statusBind.TriggerChange(); + beatmapBind.TriggerChange(); } } - private void displayMaxParticipants(int? value) - { - if (value == null) - { - participantsSlash.FadeOut(transition_duration); - maxParticipants.FadeOut(transition_duration); - } - else - { - participantsSlash.FadeIn(transition_duration); - maxParticipants.FadeIn(transition_duration); - maxParticipants.Text = value.ToString(); - } - } - - private void displayParticipants(User[] value) - { - participants.Text = value.Length.ToString(); - participantInfo.Participants = value; - participantsFlow.ChildrenEnumerable = value.Select(u => new UserTile(u)); - } - private class UserTile : Container, IHasTooltip { private readonly User user; @@ -407,5 +293,11 @@ namespace osu.Game.Screens.Multi.Components }; } } + + private class RoomStatusNoneSelected : RoomStatus + { + public override string Message => @"No Room Selected"; + public override Color4 GetAppropriateColour(OsuColour colours) => colours.Gray8; + } } } diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index db8898495f..46610a36d8 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -10,6 +10,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.SearchableList; +using osu.Game.Screens.Multi.Screens; using OpenTK; using OpenTK.Graphics; @@ -19,7 +20,7 @@ namespace osu.Game.Screens.Multi { public const float HEIGHT = 121; - private readonly OsuSpriteText screenTitle; + private readonly OsuSpriteText screenType; private readonly HeaderBreadcrumbControl breadcrumbs; public Header(Screen initialScreen) @@ -66,7 +67,7 @@ namespace osu.Game.Screens.Multi Text = "multiplayer ", TextSize = 25, }, - screenTitle = new OsuSpriteText + screenType = new OsuSpriteText { TextSize = 25, Font = @"Exo2.0-Light", @@ -85,14 +86,14 @@ namespace osu.Game.Screens.Multi }, }; - breadcrumbs.Current.ValueChanged += s => screenTitle.Text = s.ToString(); + breadcrumbs.Current.ValueChanged += s => screenType.Text = ((MultiplayerScreen)s).Type.ToLower(); breadcrumbs.Current.TriggerChange(); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - screenTitle.Colour = colours.Yellow; + screenType.Colour = colours.Yellow; breadcrumbs.StripColour = colours.Green; } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index b3d393209c..7822fa68dc 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -8,7 +8,8 @@ using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; -using osu.Game.Screens.Multi.Screens; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Multi.Screens.Lounge; namespace osu.Game.Screens.Multi { @@ -25,7 +26,7 @@ namespace osu.Game.Screens.Multi RelativeSizeAxes = Axes.Both, }; - Lobby lobby; + Lounge lounge; Children = new Drawable[] { new Container @@ -52,12 +53,12 @@ namespace osu.Game.Screens.Multi { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = Header.HEIGHT }, - Child = lobby = new Lobby(), + Child = lounge = new Lounge(), }, - new Header(lobby), + new Header(lounge), }; - lobby.Exited += s => Exit(); + lounge.Exited += s => Exit(); } protected override void OnEntering(Screen last) @@ -84,6 +85,13 @@ namespace osu.Game.Screens.Multi waves.Hide(); } + protected override void LogoExiting(OsuLogo logo) + { + // the wave overlay transition takes longer than expected to run. + logo.Delay(WaveContainer.DISAPPEAR_DURATION / 2).FadeOut(); + base.LogoExiting(logo); + } + private class MultiplayerWaveContainer : WaveContainer { protected override bool StartHidden => true; diff --git a/osu.Game/Screens/Multi/Screens/Lobby.cs b/osu.Game/Screens/Multi/Screens/Lobby.cs deleted file mode 100644 index dcda40e0d7..0000000000 --- a/osu.Game/Screens/Multi/Screens/Lobby.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; - -namespace osu.Game.Screens.Multi.Screens -{ - public class Lobby : ScreenWhiteBox - { - protected override IEnumerable PossibleChildren => new[] { - typeof(MatchCreate), - typeof(Match) - }; - } -} diff --git a/osu.Game/Screens/Multi/Screens/Lounge/FilterControl.cs b/osu.Game/Screens/Multi/Screens/Lounge/FilterControl.cs new file mode 100644 index 0000000000..421c89479a --- /dev/null +++ b/osu.Game/Screens/Multi/Screens/Lounge/FilterControl.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.SearchableList; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Multi.Screens.Lounge +{ + public class FilterControl : SearchableListFilterControl + { + protected override Color4 BackgroundColour => OsuColour.FromHex(@"362e42"); + protected override LoungeTab DefaultTab => LoungeTab.Public; + + public FilterControl() + { + DisplayStyleControl.Hide(); + } + } + + public enum LoungeTab + { + Public = RoomAvailability.Public, + Private = RoomAvailability.FriendsOnly, + } +} diff --git a/osu.Game/Screens/Multi/Screens/Lounge/Lounge.cs b/osu.Game/Screens/Multi/Screens/Lounge/Lounge.cs new file mode 100644 index 0000000000..51dea355bf --- /dev/null +++ b/osu.Game/Screens/Multi/Screens/Lounge/Lounge.cs @@ -0,0 +1,191 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.SearchableList; +using osu.Game.Screens.Multi.Components; +using OpenTK; + +namespace osu.Game.Screens.Multi.Screens.Lounge +{ + public class Lounge : MultiplayerScreen + { + private readonly Container content; + private readonly SearchContainer search; + + protected readonly FilterControl Filter; + protected readonly FillFlowContainer RoomsContainer; + protected readonly RoomInspector Inspector; + + public override string Title => "Lounge"; + + protected override Container TransitionContent => content; + + private IEnumerable rooms; + public IEnumerable Rooms + { + get { return rooms; } + set + { + if (Equals(value, rooms)) return; + rooms = value; + + var enumerable = rooms.ToList(); + + RoomsContainer.Children = enumerable.Select(r => new DrawableRoom(r) + { + Action = didSelect, + }).ToList(); + + if (!enumerable.Contains(Inspector.Room)) + Inspector.Room = null; + + filterRooms(); + } + } + + public Lounge() + { + Children = new Drawable[] + { + Filter = new FilterControl(), + content = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Width = 0.55f, + Padding = new MarginPadding + { + Vertical = 35 - DrawableRoom.SELECTION_BORDER_WIDTH, + Right = 20 - DrawableRoom.SELECTION_BORDER_WIDTH + }, + Child = search = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = RoomsContainer = new RoomsFilterContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10 - DrawableRoom.SELECTION_BORDER_WIDTH * 2), + }, + }, + }, + Inspector = new RoomInspector + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Width = 0.45f, + }, + }, + }, + }; + + Filter.Search.Current.ValueChanged += s => filterRooms(); + Filter.Tabs.Current.ValueChanged += t => filterRooms(); + Filter.Search.Exit += Exit; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + content.Padding = new MarginPadding + { + Top = Filter.DrawHeight, + Left = SearchableListOverlay.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH, + Right = SearchableListOverlay.WIDTH_PADDING, + }; + } + + protected override void OnFocus(InputState state) + { + GetContainingInputManager().ChangeFocus(Filter.Search); + } + + protected override void OnEntering(Screen last) + { + base.OnEntering(last); + Filter.Search.HoldFocus = true; + } + + protected override bool OnExiting(Screen next) + { + Filter.Search.HoldFocus = false; + return base.OnExiting(next); + } + + protected override void OnResuming(Screen last) + { + base.OnResuming(last); + Filter.Search.HoldFocus = true; + } + + protected override void OnSuspending(Screen next) + { + base.OnSuspending(next); + Filter.Search.HoldFocus = false; + } + + private void filterRooms() + { + search.SearchTerm = Filter.Search.Current.Value ?? string.Empty; + + foreach (DrawableRoom r in RoomsContainer.Children) + { + r.MatchingFilter = r.MatchingFilter && + r.Room.Availability.Value == (RoomAvailability)Filter.Tabs.Current.Value; + } + } + + private void didSelect(DrawableRoom room) + { + RoomsContainer.Children.ForEach(c => + { + if (c != room) + c.State = SelectionState.NotSelected; + }); + + Inspector.Room = room.Room; + + // open the room if its selected and is clicked again + if (room.State == SelectionState.Selected) + Push(new Match.Match(room.Room)); + } + + private class RoomsFilterContainer : FillFlowContainer, IHasFilterableChildren + { + public IEnumerable FilterTerms => new string[] { }; + public IEnumerable FilterableChildren => Children; + + public bool MatchingFilter + { + set + { + if (value) + InvalidateLayout(); + } + } + + public RoomsFilterContainer() + { + LayoutDuration = 200; + LayoutEasing = Easing.OutQuint; + } + } + } +} diff --git a/osu.Game/Screens/Multi/Screens/Match.cs b/osu.Game/Screens/Multi/Screens/Match.cs deleted file mode 100644 index 4ba7fe9f6a..0000000000 --- a/osu.Game/Screens/Multi/Screens/Match.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.Graphics; -using osu.Framework.Screens; -using osu.Game.Screens.Backgrounds; -using osu.Game.Screens.Play; -using osu.Game.Screens.Select; -using OpenTK.Graphics; - -namespace osu.Game.Screens.Multi.Screens -{ - public class Match : ScreenWhiteBox - { - protected override IEnumerable PossibleChildren => new[] { - typeof(MatchSongSelect), - typeof(Player), - }; - - protected override BackgroundScreen CreateBackground() => new BackgroundScreenCustom(@"Backgrounds/bg4"); - - protected override void OnEntering(Screen last) - { - base.OnEntering(last); - - Background.FadeColour(Color4.DarkGray, 500); - } - - protected override bool OnExiting(Screen next) - { - Background.FadeColour(Color4.White, 500); - return base.OnExiting(next); - } - } -} diff --git a/osu.Game/Screens/Multi/Screens/Match/Header.cs b/osu.Game/Screens/Multi/Screens/Match/Header.cs new file mode 100644 index 0000000000..02e717d4be --- /dev/null +++ b/osu.Game/Screens/Multi/Screens/Match/Header.cs @@ -0,0 +1,184 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.SearchableList; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Multi.Screens.Match +{ + public class Header : Container + { + public const float HEIGHT = 200; + + private readonly Box tabStrip; + private readonly UpdateableBeatmapSetCover cover; + + public readonly PageTabControl Tabs; + + public BeatmapSetInfo BeatmapSet + { + set => cover.BeatmapSet = value; + } + + public Action OnRequestSelectBeatmap; + + public Header() + { + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + BeatmapSelectButton beatmapButton; + Children = new Drawable[] + { + cover = new UpdateableBeatmapSetCover + { + RelativeSizeAxes = Axes.Both, + Masking = true, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0), Color4.Black.Opacity(0.5f)), + }, + tabStrip = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = 1, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = 200, + Padding = new MarginPadding { Vertical = 5 }, + Child = beatmapButton = new BeatmapSelectButton + { + RelativeSizeAxes = Axes.Both, + }, + }, + Tabs = new PageTabControl + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + }, + }, + }, + }; + + beatmapButton.Action = () => OnRequestSelectBeatmap?.Invoke(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + tabStrip.Colour = colours.Yellow; + } + + private class BeatmapSelectButton : OsuClickableContainer + { + private const float corner_radius = 5; + private const float bg_opacity = 0.5f; + private const float transition_duration = 100; + + private readonly Box bg; + private readonly Container border; + + public BeatmapSelectButton() + { + Masking = true; + CornerRadius = corner_radius; + + Children = new Drawable[] + { + bg = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = bg_opacity, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = @"Exo2.0-Bold", + Text = "Select Beatmap", + }, + border = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = corner_radius, + BorderThickness = 4, + Alpha = 0, + Child = new Box // needs a child to show the border + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + border.BorderColour = colours.Yellow; + } + + protected override bool OnHover(InputState state) + { + border.FadeIn(transition_duration); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + base.OnHoverLost(state); + border.FadeOut(transition_duration); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + bg.FadeTo(0.75f, 1000, Easing.Out); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + bg.FadeTo(bg_opacity, transition_duration); + return base.OnMouseUp(state, args); + } + } + } + + public enum MatchHeaderPage + { + Settings, + Room, + } +} diff --git a/osu.Game/Screens/Multi/Screens/Match/Info.cs b/osu.Game/Screens/Multi/Screens/Match/Info.cs new file mode 100644 index 0000000000..ec93eb90b1 --- /dev/null +++ b/osu.Game/Screens/Multi/Screens/Match/Info.cs @@ -0,0 +1,210 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.SearchableList; +using osu.Game.Screens.Multi.Components; +using OpenTK; + +namespace osu.Game.Screens.Multi.Screens.Match +{ + public class Info : Container + { + public const float HEIGHT = 128; + + private readonly OsuSpriteText name, availabilityStatus; + private readonly BeatmapTypeInfo beatmapTypeInfo; + private readonly ReadyButton readyButton; + + private OsuColour colours; + + public Bindable Ready => readyButton.Ready; + + public string Name + { + set { name.Text = value; } + } + + private RoomAvailability availability; + public RoomAvailability Availability + { + set + { + if (value == availability) return; + availability = value; + + if (IsLoaded) + updateAvailabilityStatus(); + } + } + + private RoomStatus status; + public RoomStatus Status + { + set + { + if (value == status) return; + status = value; + + if (IsLoaded) + updateAvailabilityStatus(); + } + } + + public BeatmapInfo Beatmap + { + set { beatmapTypeInfo.Beatmap = value; } + } + + public GameType Type + { + set { beatmapTypeInfo.Type = value; } + } + + public Info() + { + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex(@"28242d"), + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Padding = new MarginPadding { Vertical = 20 }, + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + name = new OsuSpriteText + { + TextSize = 30, + }, + availabilityStatus = new OsuSpriteText + { + TextSize = 14, + }, + }, + }, + beatmapTypeInfo = new BeatmapTypeInfo + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + }, + }, + readyButton = new ReadyButton + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(200, 1), + Padding = new MarginPadding { Vertical = 10 }, + }, + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + this.colours = colours; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateAvailabilityStatus(); + } + + private void updateAvailabilityStatus() + { + if (status != null) + { + availabilityStatus.FadeColour(status.GetAppropriateColour(colours), 100); + availabilityStatus.Text = $"{availability.GetDescription()}, {status.Message}"; + } + } + + private class ReadyButton : TriangleButton + { + public readonly Bindable Ready = new Bindable(); + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Font = @"Exo2.0-Light", + TextSize = 30, + }; + + [BackgroundDependencyLoader] + private void load() + { + BackgroundColour = OsuColour.FromHex(@"1187aa"); + Triangles.ColourLight = OsuColour.FromHex(@"277b9c"); + Triangles.ColourDark = OsuColour.FromHex(@"1f6682"); + Triangles.TriangleScale = 1.5f; + + Container active; + Add(active = new Container + { + RelativeSizeAxes = Axes.Both, + Alpha = 0f, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.15f, + Blending = BlendingMode.Additive, + }, + }); + + Action = () => Ready.Value = !Ready.Value; + + Ready.BindValueChanged(value => + { + if (value) + { + Text = "Not Ready"; + active.FadeIn(200); + } + else + { + Text = "Ready"; + active.FadeOut(200); + } + }, true); + } + } + } +} diff --git a/osu.Game/Screens/Multi/Screens/Match/Match.cs b/osu.Game/Screens/Multi/Screens/Match/Match.cs new file mode 100644 index 0000000000..ce3f7825a4 --- /dev/null +++ b/osu.Game/Screens/Multi/Screens/Match/Match.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Select; +using osu.Game.Users; + +namespace osu.Game.Screens.Multi.Screens.Match +{ + public class Match : MultiplayerScreen + { + private readonly Room room; + private readonly Participants participants; + + private readonly Bindable nameBind = new Bindable(); + private readonly Bindable statusBind = new Bindable(); + private readonly Bindable availabilityBind = new Bindable(); + private readonly Bindable typeBind = new Bindable(); + private readonly Bindable beatmapBind = new Bindable(); + private readonly Bindable maxParticipantsBind = new Bindable(); + private readonly Bindable> participantsBind = new Bindable>(); + + protected override Container TransitionContent => participants; + + public override string Type => "room"; + public override string Title => room.Name.Value; + + public Match(Room room) + { + this.room = room; + Header header; + Info info; + + Children = new Drawable[] + { + header = new Header(), + info = new Info + { + Margin = new MarginPadding { Top = Header.HEIGHT }, + }, + participants = new Participants + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = Header.HEIGHT + Info.HEIGHT }, + }, + }; + + header.OnRequestSelectBeatmap = () => Push(new MatchSongSelect()); + + beatmapBind.BindTo(room.Beatmap); + beatmapBind.BindValueChanged(b => + { + header.BeatmapSet = b?.BeatmapSet; + info.Beatmap = b; + }, true); + + nameBind.BindTo(room.Name); + nameBind.BindValueChanged(n => info.Name = n, true); + + statusBind.BindTo(room.Status); + statusBind.BindValueChanged(s => info.Status = s, true); + + availabilityBind.BindTo(room.Availability); + availabilityBind.BindValueChanged(a => info.Availability = a, true); + + typeBind.BindTo(room.Type); + typeBind.BindValueChanged(t => info.Type = t, true); + + maxParticipantsBind.BindTo(room.MaxParticipants); + maxParticipantsBind.BindValueChanged(m => { participants.Max = m; }, true); + + participantsBind.BindTo(room.Participants); + participantsBind.BindValueChanged(p => participants.Users = p, true); + } + } +} diff --git a/osu.Game/Screens/Multi/Screens/Match/Participants.cs b/osu.Game/Screens/Multi/Screens/Match/Participants.cs new file mode 100644 index 0000000000..9fa90f8752 --- /dev/null +++ b/osu.Game/Screens/Multi/Screens/Match/Participants.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays.SearchableList; +using osu.Game.Screens.Multi.Components; +using osu.Game.Users; +using OpenTK; + +namespace osu.Game.Screens.Multi.Screens.Match +{ + public class Participants : Container + { + private readonly ParticipantCount count; + private readonly FillFlowContainer usersFlow; + + public IEnumerable Users + { + set { + usersFlow.Children = value.Select(u => new UserPanel(u) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Width = 300, + OnLoadComplete = d => d.FadeInFromZero(60), + }).ToList(); + + count.Count = value.Count(); + } + } + + public int? Max + { + set => count.Max = value; + } + + public Participants() + { + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, + Children = new Drawable[] + { + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Top = 10 }, + Children = new Drawable[] + { + count = new ParticipantCount + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + usersFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Padding = new MarginPadding { Top = 40 }, + LayoutDuration = 200, + LayoutEasing = Easing.OutQuint, + }, + }, + }, + }, + }; + } + } +} diff --git a/osu.Game/Screens/Multi/Screens/MatchCreate.cs b/osu.Game/Screens/Multi/Screens/MatchCreate.cs deleted file mode 100644 index 6b4e26d5e5..0000000000 --- a/osu.Game/Screens/Multi/Screens/MatchCreate.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; - -namespace osu.Game.Screens.Multi.Screens -{ - public class MatchCreate : ScreenWhiteBox - { - protected override IEnumerable PossibleChildren => new[] { - typeof(Match) - }; - - public MatchCreate() - { - ValidForResume = false; - } - } -} diff --git a/osu.Game/Screens/Multi/Screens/MultiplayerScreen.cs b/osu.Game/Screens/Multi/Screens/MultiplayerScreen.cs new file mode 100644 index 0000000000..00c2613d54 --- /dev/null +++ b/osu.Game/Screens/Multi/Screens/MultiplayerScreen.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Screens.Multi.Screens +{ + public abstract class MultiplayerScreen : OsuScreen + { + protected virtual Container TransitionContent => Content; + + /// + /// The type to display in the title of the . + /// + public virtual string Type => Title; + + protected override void OnEntering(Screen last) + { + base.OnEntering(last); + + Content.FadeInFromZero(WaveContainer.APPEAR_DURATION, Easing.OutQuint); + TransitionContent.FadeInFromZero(WaveContainer.APPEAR_DURATION, Easing.OutQuint); + TransitionContent.MoveToX(200).MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); + } + + protected override bool OnExiting(Screen next) + { + Content.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.OutQuint); + TransitionContent.MoveToX(200, WaveContainer.DISAPPEAR_DURATION, Easing.OutQuint); + + return base.OnExiting(next); + } + + protected override void OnResuming(Screen last) + { + base.OnResuming(last); + + Content.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint); + TransitionContent.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); + } + + protected override void OnSuspending(Screen next) + { + base.OnSuspending(next); + + Content.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.OutQuint); + TransitionContent.MoveToX(-200, WaveContainer.DISAPPEAR_DURATION, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 24945ea347..61018f9e08 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using Microsoft.EntityFrameworkCore.Internal; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -17,13 +18,22 @@ using osu.Game.Rulesets; using osu.Game.Screens.Menu; using OpenTK; using OpenTK.Input; +using osu.Game.Overlays; +using osu.Framework.Graphics.Containers; namespace osu.Game.Screens { - public abstract class OsuScreen : Screen, IKeyBindingHandler + public abstract class OsuScreen : Screen, IKeyBindingHandler, IHasDescription { public BackgroundScreen Background { get; private set; } + /// + /// A user-facing title for this screen. + /// + public virtual string Title => GetType().ShortDisplayName(); + + public string Description => Title; + protected virtual bool AllowBackButton => true; /// @@ -32,12 +42,19 @@ namespace osu.Game.Screens /// protected virtual BackgroundScreen CreateBackground() => null; - protected BindableBool ShowOverlays = new BindableBool(); + private Action updateOverlayStates; /// - /// Whether overlays should be shown when this screen is entered or resumed. + /// Whether all overlays should be hidden when this screen is entered or resumed. /// - public virtual bool ShowOverlaysOnEnter => true; + protected virtual bool HideOverlaysOnEnter => false; + + protected readonly Bindable OverlayActivationMode = new Bindable(); + + /// + /// Whether overlays should be able to be opened once this screen is entered or resumed. + /// + protected virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All; /// /// Whether this allows the cursor to be displayed. @@ -60,35 +77,28 @@ namespace osu.Game.Screens private ParallaxContainer backgroundParallaxContainer; - public WorkingBeatmap InitialBeatmap - { - set - { - if (IsLoaded) throw new InvalidOperationException($"Cannot set {nameof(InitialBeatmap)} post-load."); - Beatmap.Value = value; - } - } - protected readonly Bindable Ruleset = new Bindable(); private SampleChannel sampleExit; - [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuGameBase game, OsuGame osuGame, AudioManager audio) + [BackgroundDependencyLoader(true)] + private void load(BindableBeatmap beatmap, OsuGame osuGame, AudioManager audio) { - if (game != null) - { - //if we were given a beatmap at ctor time, we want to pass this on to the game-wide beatmap. - var localMap = Beatmap.Value; - Beatmap.BindTo(game.Beatmap); - if (localMap != null) - Beatmap.Value = localMap; - } + if (beatmap != null) + Beatmap.BindTo(beatmap); if (osuGame != null) { Ruleset.BindTo(osuGame.Ruleset); - ShowOverlays.BindTo(osuGame.ShowOverlays); + OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); + + updateOverlayStates = () => + { + if (HideOverlaysOnEnter) + osuGame.CloseAllOverlays(); + else + osuGame.Toolbar.State = Visibility.Visible; + }; } sampleExit = audio.Sample.Get(@"UI/screen-back"); @@ -209,18 +219,24 @@ namespace osu.Game.Screens logo.Anchor = Anchor.TopLeft; logo.Origin = Anchor.Centre; logo.RelativePositionAxes = Axes.None; + logo.BeatMatching = true; logo.Triangles = true; logo.Ripple = true; } private void applyArrivingDefaults(bool isResuming) { - logo.AppendAnimatingAction(() => LogoArriving(logo, isResuming), true); + logo.AppendAnimatingAction(() => + { + if (IsCurrentScreen) LogoArriving(logo, isResuming); + }, true); if (backgroundParallaxContainer != null) backgroundParallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * BackgroundParallaxAmount; - ShowOverlays.Value = ShowOverlaysOnEnter; + OverlayActivationMode.Value = InitialOverlayActivationMode; + + updateOverlayStates?.Invoke(); } private void onExitingLogo() diff --git a/osu.Game/Screens/Play/HUD/QuitButton.cs b/osu.Game/Screens/Play/HUD/QuitButton.cs new file mode 100644 index 0000000000..d0aa0dad92 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/QuitButton.cs @@ -0,0 +1,198 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using OpenTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class QuitButton : FillFlowContainer + { + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; + + private readonly Button button; + + public Action Action + { + set => button.Action = value; + } + + private readonly OsuSpriteText text; + + public QuitButton() + { + Direction = FillDirection.Horizontal; + Spacing = new Vector2(20, 0); + Margin = new MarginPadding(10); + Children = new Drawable[] + { + text = new OsuSpriteText + { + Text = "hold for menu", + Font = @"Exo2.0-Bold", + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + button = new Button + { + HoverGained = () => text.FadeIn(500, Easing.OutQuint), + HoverLost = () => text.FadeOut(500, Easing.OutQuint) + } + }; + AutoSizeAxes = Axes.Both; + } + + protected override void LoadComplete() + { + text.FadeInFromZero(500, Easing.OutQuint).Delay(1500).FadeOut(500, Easing.OutQuint); + base.LoadComplete(); + } + + private float positionalAdjust; + + protected override bool OnMouseMove(InputState state) + { + positionalAdjust = Vector2.Distance(state.Mouse.NativeState.Position, button.ScreenSpaceDrawQuad.Centre) / 200; + return base.OnMouseMove(state); + } + + protected override void Update() + { + base.Update(); + + if (text.Alpha > 0 || button.Progress.Value > 0 || button.IsHovered) + Alpha = 1; + else + Alpha = Interpolation.ValueAt( + MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 1000), + Alpha, MathHelper.Clamp(1 - positionalAdjust, 0.04f, 1), 0, 200, Easing.OutQuint); + } + + private class Button : HoldToConfirmContainer + { + private SpriteIcon icon; + private CircularProgress circularProgress; + private Circle overlayCircle; + + protected override bool AllowMultipleFires => true; + + public Action HoverGained; + public Action HoverLost; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Size = new Vector2(60); + + Child = new CircularContainer + { + Masking = true, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Alpha = 0.5f, + }, + circularProgress = new CircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = 1 + }, + overlayCircle = new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Size = new Vector2(0.9f), + }, + icon = new SpriteIcon + { + Shadow = false, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(15), + Icon = FontAwesome.fa_close + }, + } + }; + + bind(); + } + + private void bind() + { + circularProgress.Current.BindTo(Progress); + Progress.ValueChanged += v => icon.Scale = new Vector2(1 + (float)v * 0.2f); + } + + private bool pendingAnimation; + + protected override void Confirm() + { + base.Confirm(); + + // temporarily unbind as to not look weird if releasing during confirm animation (can see the unwind of progress). + Progress.UnbindAll(); + + // avoid starting a new confirm call until we finish animating. + pendingAnimation = true; + + Progress.Value = 0; + + overlayCircle.ScaleTo(0, 100) + .Then().FadeOut().ScaleTo(1).FadeIn(500) + .OnComplete(a => + { + icon.ScaleTo(1, 100); + circularProgress.FadeOut(100).OnComplete(_ => + { + bind(); + + circularProgress.FadeIn(); + pendingAnimation = false; + }); + }); + } + + protected override bool OnHover(InputState state) + { + HoverGained?.Invoke(); + return true; + } + + protected override void OnHoverLost(InputState state) + { + HoverLost?.Invoke(); + base.OnHoverLost(state); + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + if (!pendingAnimation && state.Mouse.Buttons.Count == 1) + BeginConfirm(); + return true; + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + if (state.Mouse.Buttons.Count == 0) + AbortConfirm(); + return true; + } + } + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 36d8bb75c0..f920b20649 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -34,6 +34,7 @@ namespace osu.Game.Screens.Play public readonly HealthDisplay HealthDisplay; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; + public readonly QuitButton HoldToQuit; public readonly PlayerSettingsOverlay PlayerSettingsOverlay; private Bindable showHud; @@ -51,14 +52,26 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { - KeyCounter = CreateKeyCounter(), ComboCounter = CreateComboCounter(), ScoreCounter = CreateScoreCounter(), AccuracyCounter = CreateAccuracyCounter(), HealthDisplay = CreateHealthDisplay(), Progress = CreateProgress(), ModDisplay = CreateModsContainer(), - PlayerSettingsOverlay = CreatePlayerSettingsOverlay() + PlayerSettingsOverlay = CreatePlayerSettingsOverlay(), + new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + KeyCounter = CreateKeyCounter(), + HoldToQuit = CreateQuitButton(), + } + } } }); @@ -187,7 +200,6 @@ namespace osu.Game.Screens.Play Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, Margin = new MarginPadding(10), - Y = -TwoLayerButton.SIZE_RETRACTED.Y, }; protected virtual ScoreCounter CreateScoreCounter() => new ScoreCounter(6) @@ -205,6 +217,12 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.X, }; + protected virtual QuitButton CreateQuitButton() => new QuitButton + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }; + protected virtual ModDisplay CreateModsContainer() => new ModDisplay { Anchor = Anchor.TopRight, diff --git a/osu.Game/Screens/Play/KeyCounterCollection.cs b/osu.Game/Screens/Play/KeyCounterCollection.cs index 8cbb9986e5..114ea83ba6 100644 --- a/osu.Game/Screens/Play/KeyCounterCollection.cs +++ b/osu.Game/Screens/Play/KeyCounterCollection.cs @@ -18,7 +18,8 @@ namespace osu.Game.Screens.Play { private const int duration = 100; - private Bindable showKeyCounter; + public readonly Bindable Visible = new Bindable(true); + private readonly Bindable configVisibility = new Bindable(); public KeyCounterCollection() { @@ -46,9 +47,10 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - showKeyCounter = config.GetBindable(OsuSetting.KeyOverlay); - showKeyCounter.ValueChanged += keyCounterVisibility => this.FadeTo(keyCounterVisibility ? 1 : 0, duration); - showKeyCounter.TriggerChange(); + config.BindWith(OsuSetting.KeyOverlay, configVisibility); + + Visible.BindValueChanged(_ => updateVisibility()); + configVisibility.BindValueChanged(_ => updateVisibility(), true); } //further: change default values here and in KeyCounter if needed, instead of passing them in every constructor @@ -111,6 +113,8 @@ namespace osu.Game.Screens.Play } } + private void updateVisibility() => this.FadeTo(Visible.Value || configVisibility.Value ? 1 : 0, duration); + public override bool HandleKeyboardInput => receptor == null; public override bool HandleMouseInput => receptor == null; diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs index bd334ad2e2..6262f71ddc 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PauseContainer.cs @@ -44,13 +44,18 @@ namespace osu.Game.Screens.Play public Action OnResume; public Action OnPause; - private readonly IAdjustableClock adjustableClock; private readonly FramedClock framedClock; + private readonly DecoupleableInterpolatingFramedClock decoupledClock; - public PauseContainer(FramedClock framedClock, IAdjustableClock adjustableClock) + /// + /// Creates a new . + /// + /// The gameplay clock. This is the clock that will process frames. + /// The seekable clock. This is the clock that will be paused and resumed. + public PauseContainer(FramedClock framedClock, DecoupleableInterpolatingFramedClock decoupledClock) { this.framedClock = framedClock; - this.adjustableClock = adjustableClock; + this.decoupledClock = decoupledClock; RelativeSizeAxes = Axes.Both; @@ -80,7 +85,7 @@ namespace osu.Game.Screens.Play if (IsPaused) return; // stop the seekable clock (stops the audio eventually) - adjustableClock.Stop(); + decoupledClock.Stop(); IsPaused = true; OnPause?.Invoke(); @@ -97,10 +102,10 @@ namespace osu.Game.Screens.Play IsResuming = false; lastPauseActionTime = Time.Current; - // seek back to the time of the framed clock. - // this accounts for the audio clock potentially taking time to enter a completely stopped state. - adjustableClock.Seek(framedClock.CurrentTime); - adjustableClock.Start(); + // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time + // This accounts for the audio clock source potentially taking time to enter a completely stopped state + decoupledClock.Seek(decoupledClock.CurrentTime); + decoupledClock.Start(); OnResume?.Invoke(); pauseOverlay.Hide(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 46919e25e1..a2ed01f5a7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -21,6 +22,7 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Online.API; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -35,7 +37,9 @@ namespace osu.Game.Screens.Play { protected override float BackgroundParallaxAmount => 0.1f; - public override bool ShowOverlaysOnEnter => false; + protected override bool HideOverlaysOnEnter => true; + + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; public Action RestartRequested; @@ -70,7 +74,7 @@ namespace osu.Game.Screens.Play private SampleChannel sampleRestart; - private ScoreProcessor scoreProcessor; + protected ScoreProcessor ScoreProcessor; protected RulesetContainer RulesetContainer; private HUDOverlay hudOverlay; @@ -117,7 +121,7 @@ namespace osu.Game.Screens.Play // let's try again forcing the beatmap's ruleset. ruleset = beatmap.BeatmapInfo.Ruleset; rulesetInstance = ruleset.CreateInstance(); - RulesetContainer = rulesetInstance.CreateRulesetContainerWith(Beatmap); + RulesetContainer = rulesetInstance.CreateRulesetContainerWith(Beatmap.Value); } if (!RulesetContainer.Objects.Any()) @@ -143,13 +147,18 @@ namespace osu.Game.Screens.Play adjustableClock.ProcessFrame(); + // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. + // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 }; + // the final usable gameplay clock with user-set offsets applied. - var offsetClock = new FramedOffsetClock(adjustableClock); + var offsetClock = new FramedOffsetClock(platformOffsetClock); userAudioOffset.ValueChanged += v => offsetClock.Offset = v; userAudioOffset.TriggerChange(); - scoreProcessor = RulesetContainer.CreateScoreProcessor(); + ScoreProcessor = RulesetContainer.CreateScoreProcessor(); + config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); Children = new Drawable[] { @@ -176,21 +185,21 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both, Child = RulesetContainer }, - new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, scoreProcessor) + new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { Anchor = Anchor.Centre, Origin = Anchor.Centre, ProcessCustomClock = false, Breaks = beatmap.Breaks }, - hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, working, offsetClock, adjustableClock) + RulesetContainer.Cursor?.CreateProxy() ?? new Container(), + hudOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, offsetClock, adjustableClock) { Clock = Clock, // hud overlay doesn't want to use the audio clock directly ProcessCustomClock = false, Anchor = Anchor.Centre, Origin = Anchor.Centre }, - RulesetContainer.Cursor?.CreateProxy() ?? new Container(), new SkipOverlay(firstObjectTime) { Clock = Clock, // skip button doesn't want to use the audio clock directly @@ -219,15 +228,18 @@ namespace osu.Game.Screens.Play } }; + hudOverlay.HoldToQuit.Action = Exit; + hudOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded); + if (ShowStoryboard) initializeStoryboard(false); // Bind ScoreProcessor to ourselves - scoreProcessor.AllJudged += onCompletion; - scoreProcessor.Failed += onFail; + ScoreProcessor.AllJudged += onCompletion; + ScoreProcessor.Failed += onFail; foreach (var mod in Beatmap.Value.Mods.Value.OfType()) - mod.ApplyToScoreProcessor(scoreProcessor); + mod.ApplyToScoreProcessor(ScoreProcessor); } private void applyRateFromMods() @@ -252,7 +264,7 @@ namespace osu.Game.Screens.Play private void onCompletion() { // Only show the completion screen if the player hasn't failed - if (scoreProcessor.HasFailed || onCompletionEvent != null) + if (ScoreProcessor.HasFailed || onCompletionEvent != null) return; ValidForResume = false; @@ -270,7 +282,7 @@ namespace osu.Game.Screens.Play Beatmap = Beatmap.Value.BeatmapInfo, Ruleset = ruleset }; - scoreProcessor.PopulateScore(score); + ScoreProcessor.PopulateScore(score); score.User = RulesetContainer.Replay?.User ?? api.LocalUser.Value; Push(new Results(score)); }); @@ -362,7 +374,7 @@ namespace osu.Game.Screens.Play Background?.FadeTo(1f, fade_out_duration); } - protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value && !pauseContainer.IsPaused; + protected override bool OnScroll(InputState state) => mouseWheelDisabled.Value && !pauseContainer.IsPaused; private void initializeStoryboard(bool asyncLoad) { diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 56fbd7b6e7..6e1c3d24f0 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -7,15 +7,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; +using osu.Framework.Localisation; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using OpenTK; -using osu.Framework.Localisation; -using osu.Framework.Threading; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; +using OpenTK; namespace osu.Game.Screens.Play { @@ -25,8 +25,8 @@ namespace osu.Game.Screens.Play private BeatmapMetadataDisplay info; - private bool showOverlays = true; - public override bool ShowOverlaysOnEnter => showOverlays; + private bool hideOverlays; + protected override bool HideOverlaysOnEnter => hideOverlays; private Task loadTask; @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Play player.RestartRequested = () => { - showOverlays = false; + hideOverlays = true; ValidForResume = true; }; } @@ -44,18 +44,26 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load() { - Add(info = new BeatmapMetadataDisplay(Beatmap) + Add(info = new BeatmapMetadataDisplay(Beatmap.Value) { Alpha = 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - Add(new VisualSettings + Add(new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Margin = new MarginPadding(25) + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding(25), + Children = new PlayerSettingsGroup[] + { + new VisualSettings(), + new InputSettings() + } }); loadTask = LoadComponentAsync(player); @@ -201,8 +209,11 @@ namespace osu.Game.Screens.Play { base.Dispose(isDisposing); - // if the player never got pushed, we should explicitly dispose it. - loadTask?.ContinueWith(_ => player.Dispose()); + if (isDisposing) + { + // if the player never got pushed, we should explicitly dispose it. + loadTask?.ContinueWith(_ => player.Dispose()); + } } private class BeatmapMetadataDisplay : Container diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs new file mode 100644 index 0000000000..755ba468cc --- /dev/null +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Configuration; + +namespace osu.Game.Screens.Play.PlayerSettings +{ + public class InputSettings : PlayerSettingsGroup + { + protected override string Title => "Input settings"; + + private readonly PlayerCheckbox mouseButtonsCheckbox; + + public InputSettings() + { + Children = new Drawable[] + { + mouseButtonsCheckbox = new PlayerCheckbox + { + LabelText = "Disable mouse buttons" + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) => mouseButtonsCheckbox.Bindable = config.GetBindable(OsuSetting.MouseDisableButtons); + } +} diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs index 0ffcdf94a6..cff3eca895 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs @@ -50,11 +50,11 @@ namespace osu.Game.Screens.Play.PlayerSettings content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); } - button.FadeColour(expanded ? buttonActiveColour : Color4.White, 200, Easing.OutQuint); + updateExpanded(); } } - private Color4 buttonActiveColour; + private Color4 expandedColour; protected PlayerSettingsGroup() { @@ -130,9 +130,13 @@ namespace osu.Game.Screens.Play.PlayerSettings [BackgroundDependencyLoader] private void load(OsuColour colours) { - button.Colour = buttonActiveColour = colours.Yellow; + expandedColour = colours.Yellow; + + updateExpanded(); } + private void updateExpanded() => button.FadeColour(expanded ? expandedColour : Color4.White, 200, Easing.InOutQuint); + protected override Container Content => content; protected override bool OnHover(InputState state) => true; diff --git a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs index 1ccc5e2fe8..7f18305b1c 100644 --- a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs +++ b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Play { public abstract class ScreenWithBeatmapBackground : OsuScreen { - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); public override bool AllowBeatmapRulesetChange => false; diff --git a/osu.Game/Screens/Play/SongProgressInfo.cs b/osu.Game/Screens/Play/SongProgressInfo.cs index b79c212ade..3156a646db 100644 --- a/osu.Game/Screens/Play/SongProgressInfo.cs +++ b/osu.Game/Screens/Play/SongProgressInfo.cs @@ -92,6 +92,6 @@ namespace osu.Game.Screens.Play } } - private string formatTime(TimeSpan timeSpan) => $"{(timeSpan < TimeSpan.Zero ? "-" : "")}{timeSpan.Duration().TotalMinutes:N0}:{timeSpan.Duration().Seconds:D2}"; + private string formatTime(TimeSpan timeSpan) => $"{(timeSpan < TimeSpan.Zero ? "-" : "")}{Math.Floor(timeSpan.Duration().TotalMinutes)}:{timeSpan.Duration().Seconds:D2}"; } } diff --git a/osu.Game/Screens/Ranking/Results.cs b/osu.Game/Screens/Ranking/Results.cs index 32161a0b8e..56a270f559 100644 --- a/osu.Game/Screens/Ranking/Results.cs +++ b/osu.Game/Screens/Ranking/Results.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Ranking private static readonly Vector2 background_blur = new Vector2(20); - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); private const float overscan = 1.3f; @@ -57,6 +57,7 @@ namespace osu.Game.Screens.Ranking { base.OnEntering(last); (Background as BackgroundScreenBeatmap)?.BlurTo(background_blur, 2500, Easing.OutQuint); + Background.ScaleTo(1.1f, transition_time, Easing.OutQuint); allCircles.ForEach(c => { @@ -102,6 +103,8 @@ namespace osu.Game.Screens.Ranking c.ScaleTo(0, transition_time, Easing.OutSine); }); + Background.ScaleTo(1f, transition_time / 4, Easing.OutQuint); + Content.FadeOut(transition_time / 4); return base.OnExiting(next); @@ -160,7 +163,6 @@ namespace osu.Game.Screens.Ranking { RelativeSizeAxes = Axes.Both, ParallaxAmount = 0.01f, - Scale = new Vector2(1 / circle_outer_scale / overscan), Anchor = Anchor.Centre, Origin = Anchor.Centre, Children = new Drawable[] @@ -272,10 +274,10 @@ namespace osu.Game.Screens.Ranking switch (mode) { case ResultMode.Summary: - currentPage = new ResultsPageScore(score, Beatmap); + currentPage = new ResultsPageScore(score, Beatmap.Value); break; case ResultMode.Ranking: - currentPage = new ResultsPageRanking(score, Beatmap); + currentPage = new ResultsPageRanking(score, Beatmap.Value); break; } diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index ca36f94eda..f1bd2b945f 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -6,7 +6,6 @@ using OpenTK.Graphics; 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.Graphics.UserInterface; using System.Linq; @@ -120,14 +119,8 @@ namespace osu.Game.Screens.Select Margin = new MarginPadding { Top = spacing * 2 }, Children = new[] { - description = new MetadataSection("Description") - { - TextColour = Color4.White.Opacity(0.75f), - }, - source = new MetadataSection("Source") - { - TextColour = Color4.White.Opacity(0.75f), - }, + description = new MetadataSection("Description"), + source = new MetadataSection("Source"), tags = new MetadataSection("Tags"), }, }, @@ -164,10 +157,9 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(OsuColour colours, APIAccess api) + private void load(APIAccess api) { this.api = api; - tags.TextColour = colours.Yellow; } protected override void UpdateAfterChildren() @@ -364,7 +356,7 @@ namespace osu.Game.Screens.Select { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Colour = textFlow.Colour, + Colour = Color4.White.Opacity(0.75f), Text = text }, loaded => { @@ -375,12 +367,6 @@ namespace osu.Game.Screens.Select 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 97f6371cb2..7950018554 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select { if (osuGame != null) ruleset.BindTo(osuGame.Ruleset); - ruleset.ValueChanged += updateRuleset; + ruleset.ValueChanged += _ => updateDisplay(); } protected override bool BlockPassThroughMouse => false; @@ -78,66 +78,76 @@ namespace osu.Game.Screens.Select private WorkingBeatmap beatmap; - public void UpdateBeatmap(WorkingBeatmap beatmap) + public WorkingBeatmap Beatmap { - this.beatmap = beatmap; - loadBeatmap(); + get => beatmap; + set + { + if (beatmap == value) return; + + beatmap = value; + updateDisplay(); + } } - private void updateRuleset(RulesetInfo ruleset) => loadBeatmap(); + private BufferedWedgeInfo loadingInfo; - private void loadBeatmap() + private void updateDisplay() { - void updateState() + void removeOldInfo() { State = beatmap == null ? Visibility.Hidden : Visibility.Visible; Info?.FadeOut(250); Info?.Expire(); + Info = null; } if (beatmap == null) { - updateState(); + removeOldInfo(); return; } - LoadComponentAsync(new BufferedWedgeInfo(beatmap, ruleset.Value) + LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value) { Shear = -Shear, - Depth = Info?.Depth + 1 ?? 0, - }, newInfo => + Depth = Info?.Depth + 1 ?? 0 + }, loaded => { - updateState(); - Add(Info = newInfo); + // ensure we are the most recent loaded wedge. + if (loaded != loadingInfo) return; + + removeOldInfo(); + Add(Info = loaded); }); } public class BufferedWedgeInfo : BufferedContainer { - private readonly WorkingBeatmap working; public OsuSpriteText VersionLabel { get; private set; } public OsuSpriteText TitleLabel { get; private set; } public OsuSpriteText ArtistLabel { get; private set; } public FillFlowContainer MapperContainer { get; private set; } public FillFlowContainer InfoLabelContainer { get; private set; } + private UnicodeBindableString titleBinding; private UnicodeBindableString artistBinding; + private readonly WorkingBeatmap beatmap; private readonly RulesetInfo ruleset; - public BufferedWedgeInfo(WorkingBeatmap working, RulesetInfo userRuleset) + public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset) { - this.working = working; - - ruleset = userRuleset ?? working.BeatmapInfo.Ruleset; + this.beatmap = beatmap; + ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; } [BackgroundDependencyLoader] private void load(LocalisationEngine localisation) { - var beatmapInfo = working.BeatmapInfo; - var metadata = beatmapInfo.Metadata ?? working.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); + var beatmapInfo = beatmap.BeatmapInfo; + var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); PixelSnapping = true; CacheDrawnFrameBuffer = true; @@ -165,7 +175,7 @@ namespace osu.Game.Screens.Select Children = new[] { // Zoomed-in and cropped beatmap background - new BeatmapBackgroundSprite(working) + new BeatmapBackgroundSprite(beatmap) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -248,27 +258,27 @@ namespace osu.Game.Screens.Select private InfoLabel[] getInfoLabels() { - var beatmap = working.Beatmap; + var b = beatmap.Beatmap; List labels = new List(); - if (beatmap?.HitObjects?.Any() == true) + if (b?.HitObjects?.Any() == true) { - HitObject lastObject = beatmap.HitObjects.LastOrDefault(); + HitObject lastObject = b.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 = TimeSpan.FromMilliseconds(endTime - beatmap.HitObjects.First().StartTime).ToString(@"m\:ss"), + Content = TimeSpan.FromMilliseconds(endTime - b.HitObjects.First().StartTime).ToString(@"m\:ss"), })); labels.Add(new InfoLabel(new BeatmapStatistic { Name = "BPM", Icon = FontAwesome.fa_circle, - Content = getBPMRange(beatmap), + Content = getBPMRange(b), })); IBeatmap playableBeatmap; @@ -276,12 +286,12 @@ namespace osu.Game.Screens.Select try { // Try to get the beatmap with the user's ruleset - playableBeatmap = working.GetPlayableBeatmap(ruleset); + playableBeatmap = beatmap.GetPlayableBeatmap(ruleset); } catch (BeatmapInvalidForRulesetException) { // Can't be converted to the user's ruleset, so use the beatmap's own ruleset - playableBeatmap = working.GetPlayableBeatmap(working.BeatmapInfo.Ruleset); + playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset); } labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s))); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index f39952dc31..c5996327b9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Select.Carousel if (songSelect != null) { - startRequested = songSelect.FinaliseSelection; + startRequested = b => songSelect.FinaliseSelection(b); editRequested = songSelect.Edit; } diff --git a/osu.Game/Screens/Select/EditSongSelect.cs b/osu.Game/Screens/Select/EditSongSelect.cs index bca009e2c1..e1d71fdd05 100644 --- a/osu.Game/Screens/Select/EditSongSelect.cs +++ b/osu.Game/Screens/Select/EditSongSelect.cs @@ -7,7 +7,7 @@ namespace osu.Game.Screens.Select { protected override bool ShowFooter => false; - protected override bool OnSelectionFinalised() + protected override bool OnStart() { Exit(); return true; diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index ee458a13a4..f9f3db3827 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -190,7 +190,5 @@ namespace osu.Game.Screens.Select protected override bool OnMouseMove(InputState state) => true; protected override bool OnClick(InputState state) => true; - - protected override bool OnDragStart(InputState state) => true; } } diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 363249ab63..8f07e0a763 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -141,7 +141,5 @@ namespace osu.Game.Screens.Select protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; protected override bool OnClick(InputState state) => true; - - protected override bool OnDragStart(InputState state) => true; } } diff --git a/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs index baab973ae5..19732107c7 100644 --- a/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Game.Graphics; @@ -142,6 +143,8 @@ namespace osu.Game.Screens.Select.Leaderboards { flagBadgeContainer = new Container { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, Size = new Vector2(87f, 20f), Masking = true, Children = new Drawable[] @@ -155,14 +158,16 @@ namespace osu.Game.Screens.Select.Leaderboards }, new FillFlowContainer { + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(10f, 0f), - Margin = new MarginPadding { Left = edge_margin, }, + Margin = new MarginPadding { Left = edge_margin }, Children = new Drawable[] { - maxCombo = new ScoreComponentLabel(FontAwesome.fa_link, Score.MaxCombo.ToString()), - accuracy = new ScoreComponentLabel(FontAwesome.fa_crosshairs, string.Format(Score.Accuracy % 1 == 0 ? @"{0:P0}" : @"{0:P2}", Score.Accuracy)), + maxCombo = new ScoreComponentLabel(FontAwesome.fa_link, Score.MaxCombo.ToString(), "Max Combo"), + accuracy = new ScoreComponentLabel(FontAwesome.fa_crosshairs, string.Format(Score.Accuracy % 1 == 0 ? @"{0:P0}" : @"{0:P2}", Score.Accuracy), "Accuracy"), }, }, }, @@ -305,37 +310,61 @@ namespace osu.Game.Screens.Select.Leaderboards } } - private class ScoreComponentLabel : Container + private class ScoreComponentLabel : Container, IHasTooltip { - public ScoreComponentLabel(FontAwesome icon, string value) - { - Anchor = Anchor.CentreLeft; - Origin = Anchor.CentreLeft; - Size = new Vector2(60f, 20f); - Padding = new MarginPadding { Top = 10f, }; + private const float icon_size = 20; - Children = new Drawable[] + private readonly string name; + private readonly FillFlowContainer content; + + public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos); + + public string TooltipText => name; + + public ScoreComponentLabel(FontAwesome icon, string value, string name) + { + this.name = name; + AutoSizeAxes = Axes.Y; + Width = 60; + + Child = content = new FillFlowContainer { - new SpriteIcon + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] { - Origin = Anchor.Centre, - Icon = FontAwesome.fa_square, - Colour = OsuColour.FromHex(@"3087ac"), - Rotation = 45, - Size = new Vector2(20), - Shadow = true, - }, - new SpriteIcon - { - Origin = Anchor.Centre, - Icon = icon, - Colour = OsuColour.FromHex(@"a4edff"), - Size = new Vector2(14), - }, - new GlowingSpriteText(value, @"Exo2.0-Bold", 17, Color4.White, OsuColour.FromHex(@"83ccfa")) - { - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = 15, }, + new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Children = new[] + { + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size), + Rotation = 45, + Colour = OsuColour.FromHex(@"3087ac"), + Icon = FontAwesome.fa_square, + Shadow = true, + }, + new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(icon_size - 6), + Colour = OsuColour.FromHex(@"a4edff"), + Icon = icon, + }, + }, + }, + new GlowingSpriteText(value, @"Exo2.0-Bold", 17, Color4.White, OsuColour.FromHex(@"83ccfa")) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, }, }; } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 3ffac591f3..339392d5cf 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -5,14 +5,9 @@ namespace osu.Game.Screens.Select { public class MatchSongSelect : SongSelect { - protected override bool OnSelectionFinalised() + protected override bool OnStart() { - Schedule(() => - { - // needs to be scheduled else we enter an infinite feedback loop. - if (IsCurrentScreen) Exit(); - }); - + if (IsCurrentScreen) Exit(); return true; } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 7992930c45..8ce40fcfa0 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Select return false; } - protected override bool OnSelectionFinalised() + protected override bool OnStart() { if (player != null) return false; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 4ffa9e2a90..70b473bcd9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Threading; using OpenTK; using OpenTK.Input; using osu.Framework.Allocation; @@ -63,10 +62,9 @@ namespace osu.Game.Screens.Select private SampleChannel sampleChangeDifficulty; private SampleChannel sampleChangeBeatmap; - private CancellationTokenSource initialAddSetsTask; - private DependencyContainer dependencies; - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent); + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); protected SongSelect() { @@ -178,7 +176,7 @@ namespace osu.Game.Screens.Select } } - [BackgroundDependencyLoader(permitNulls: true)] + [BackgroundDependencyLoader(true)] private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours) { dependencies.CacheAs(this); @@ -207,19 +205,15 @@ namespace osu.Game.Screens.Select sampleChangeDifficulty = audio.Sample.Get(@"SongSelect/select-difficulty"); sampleChangeBeatmap = audio.Sample.Get(@"SongSelect/select-expand"); - initialAddSetsTask = new CancellationTokenSource(); + Carousel.BeatmapSets = this.beatmaps.GetAllUsableBeatmapSetsEnumerable(); - Carousel.BeatmapSets = this.beatmaps.GetAllUsableBeatmapSets(); - - Beatmap.DisabledChanged += disabled => Carousel.AllowSelection = !disabled; - Beatmap.TriggerChange(); - - Beatmap.ValueChanged += workingBeatmapChanged; + Beatmap.BindDisabledChanged(disabled => Carousel.AllowSelection = !disabled, true); + Beatmap.BindValueChanged(workingBeatmapChanged); } public void Edit(BeatmapInfo beatmap) { - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap.Value); Push(new Editor()); } @@ -227,7 +221,8 @@ namespace osu.Game.Screens.Select /// Call to make a selection and perform the default action for this SongSelect. /// /// An optional beatmap to override the current carousel selection. - public void FinaliseSelection(BeatmapInfo beatmap = null) + /// Whether to trigger . + public void FinaliseSelection(BeatmapInfo beatmap = null, bool performStartAction = true) { // if we have a pending filter operation, we want to run it now. // it could change selection (ie. if the ruleset has been changed). @@ -243,14 +238,15 @@ namespace osu.Game.Screens.Select selectionChangedDebounce = null; } - OnSelectionFinalised(); + if (performStartAction) + OnStart(); } /// /// Called when a selection is made. /// /// If a resultant action occurred that takes the user away from SongSelect. - protected abstract bool OnSelectionFinalised(); + protected abstract bool OnStart(); private ScheduledDelegate selectionChangedDebounce; @@ -283,7 +279,7 @@ namespace osu.Game.Screens.Select { bool preview = beatmap?.BeatmapSetInfoID != Beatmap.Value?.BeatmapInfo.BeatmapSetInfoID; - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap.Value); ensurePlayingSelected(preview); } @@ -370,7 +366,7 @@ namespace osu.Game.Screens.Select { if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) { - UpdateBeatmap(Beatmap); + UpdateBeatmap(Beatmap.Value); ensurePlayingSelected(); } @@ -395,7 +391,7 @@ namespace osu.Game.Screens.Select protected override bool OnExiting(Screen next) { - FinaliseSelection(); + FinaliseSelection(performStartAction: false); beatmapInfoWedge.State = Visibility.Hidden; @@ -417,8 +413,6 @@ namespace osu.Game.Screens.Select beatmaps.BeatmapHidden -= onBeatmapHidden; beatmaps.BeatmapRestored -= onBeatmapRestored; } - - initialAddSetsTask?.Cancel(); } /// @@ -435,7 +429,7 @@ namespace osu.Game.Screens.Select backgroundModeBeatmap.FadeTo(1, 250); } - beatmapInfoWedge.UpdateBeatmap(beatmap); + beatmapInfoWedge.Beatmap = beatmap; } private void ensurePlayingSelected(bool preview = false) diff --git a/osu.Game/Screens/Tournament/Drawings.cs b/osu.Game/Screens/Tournament/Drawings.cs index 1ef0b6cca0..29301899d5 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"; - public override bool ShowOverlaysOnEnter => false; + protected override bool HideOverlaysOnEnter => true; protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 135b5e2b1e..d15f3053a3 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Textures; using System.Linq; +using osu.Game.Beatmaps; namespace osu.Game.Storyboards.Drawables { @@ -63,14 +64,14 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(OsuGameBase game, TextureStore textureStore) + private void load(IBindableBeatmap beatmap, TextureStore textureStore) { var basePath = Animation.Path.ToLowerInvariant(); for (var frame = 0; frame < Animation.FrameCount; frame++) { var framePath = basePath.Replace(".", frame + "."); - var path = game.Beatmap.Value.BeatmapSetInfo.Files.FirstOrDefault(f => f.Filename.ToLowerInvariant() == framePath)?.FileInfo.StoragePath; + var path = beatmap.Value.BeatmapSetInfo.Files.FirstOrDefault(f => f.Filename.ToLowerInvariant() == framePath)?.FileInfo.StoragePath; if (path == null) continue; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 9f22bebcc2..efbb3de253 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using System.Linq; +using osu.Game.Beatmaps; namespace osu.Game.Storyboards.Drawables { @@ -62,10 +63,10 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(OsuGameBase game, TextureStore textureStore) + private void load(IBindableBeatmap beatmap, TextureStore textureStore) { var spritePath = Sprite.Path.ToLowerInvariant(); - var path = game.Beatmap.Value.BeatmapSetInfo.Files.FirstOrDefault(f => f.Filename.ToLowerInvariant() == spritePath)?.FileInfo.StoragePath; + var path = beatmap.Value.BeatmapSetInfo.Files.FirstOrDefault(f => f.Filename.ToLowerInvariant() == spritePath)?.FileInfo.StoragePath; if (path == null) return; diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 7470f6ebed..cf4dda52a8 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -16,7 +16,8 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Tests.Beatmaps { [TestFixture] - public abstract class BeatmapConversionTest + public abstract class BeatmapConversionTest + where TConvertMapping : ConvertMapping, IEquatable, new() where TConvertValue : IEquatable { private const string resource_namespace = "Testing.Beatmaps"; @@ -59,9 +60,13 @@ namespace osu.Game.Tests.Beatmaps else if (objectCounter >= expectedMapping.Objects.Count) Assert.Fail($"The conversion generated a hitobject, but should not have, for hitobject at time: {ourMapping.StartTime}:\n" + $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n"); - else if (!EqualityComparer.Default.Equals(expectedMapping.Objects[objectCounter], ourMapping.Objects[objectCounter])) + else if (!expectedMapping.Equals(ourMapping)) + Assert.Fail($"The conversion mapping differed for object at time {expectedMapping.StartTime}:\n" + + $"Expected {JsonConvert.SerializeObject(expectedMapping)}\n" + + $"Received: {JsonConvert.SerializeObject(ourMapping)}\n"); + else if (!expectedMapping.Objects[objectCounter].Equals(ourMapping.Objects[objectCounter])) { - Assert.Fail($"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}\n" + Assert.Fail($"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}:\n" + $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n" + $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n"); } @@ -84,19 +89,22 @@ namespace osu.Game.Tests.Beatmaps beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo(); var result = new ConvertResult(); - var converter = rulesetInstance.CreateBeatmapConverter(beatmap); + converter.ObjectConverted += (orig, converted) => { converted.ForEach(h => h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty)); - var mapping = new ConvertMapping { StartTime = orig.StartTime }; + var mapping = CreateConvertMapping(); + mapping.StartTime = orig.StartTime; + foreach (var obj in converted) mapping.Objects.AddRange(CreateConvertValue(obj)); result.Mappings.Add(mapping); }; - converter.Convert(); + IBeatmap convertedBeatmap = converter.Convert(); + rulesetInstance.CreateBeatmapProcessor(convertedBeatmap)?.PostProcess(); return result; } @@ -128,21 +136,54 @@ namespace osu.Game.Tests.Beatmaps return Assembly.LoadFrom(Path.Combine(localPath, $"{ResourceAssembly}.dll")).GetManifestResourceStream($@"{ResourceAssembly}.Resources.{name}"); } - protected abstract IEnumerable CreateConvertValue(HitObject hitObject); - protected abstract Ruleset CreateRuleset(); + /// + /// Creates the conversion mapping for a . A conversion mapping stores important information about the conversion process. + /// This is generated _after_ the has been converted. + /// + /// This should be used to validate the integrity of the conversion process after a conversion has occurred. + /// + /// + protected virtual TConvertMapping CreateConvertMapping() => new TConvertMapping(); - private class ConvertMapping - { - [JsonProperty] - public double StartTime; - [JsonProperty] - public List Objects = new List(); - } + /// + /// Creates the conversion value for a . A conversion value stores information about the converted . + /// + /// This should be used to validate the integrity of the converted . + /// + /// + /// The converted . + protected abstract IEnumerable CreateConvertValue(HitObject hitObject); + + /// + /// Creates the applicable to this . + /// + /// + protected abstract Ruleset CreateRuleset(); private class ConvertResult { [JsonProperty] - public List Mappings = new List(); + public List Mappings = new List(); } } + + public abstract class BeatmapConversionTest : BeatmapConversionTest, TConvertValue> + where TConvertValue : IEquatable + { + } + + public class ConvertMapping : IEquatable> + where TConvertValue : IEquatable + { + [JsonProperty] + public double StartTime; + + [JsonIgnore] + public List Objects = new List(); + + [JsonProperty("Objects")] + private List setObjects { set => Objects = value; } + + public virtual bool Equals(ConvertMapping other) => StartTime.Equals(other?.StartTime); + } } diff --git a/osu.Game/Tests/Visual/EditorClockTestCase.cs b/osu.Game/Tests/Visual/EditorClockTestCase.cs index 43b20f7021..521b51529e 100644 --- a/osu.Game/Tests/Visual/EditorClockTestCase.cs +++ b/osu.Game/Tests/Visual/EditorClockTestCase.cs @@ -20,29 +20,26 @@ namespace osu.Game.Tests.Visual protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor(); protected readonly EditorClock Clock; - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) - => dependencies = new DependencyContainer(parent); - - private OsuGameBase osuGame; - protected EditorClockTestCase() { - Clock = new EditorClock(new ControlPointInfo(), BeatDivisor) { IsCoupled = false }; + Clock = new EditorClock(new ControlPointInfo(), 5000, BeatDivisor) { IsCoupled = false }; } - [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) { - this.osuGame = osuGame; + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); dependencies.Cache(BeatDivisor); dependencies.CacheAs(Clock); dependencies.CacheAs(Clock); - osuGame.Beatmap.ValueChanged += beatmapChanged; - beatmapChanged(osuGame.Beatmap.Value); + return dependencies; + } + + [BackgroundDependencyLoader] + private void load() + { + Beatmap.BindValueChanged(beatmapChanged, true); } private void beatmapChanged(WorkingBeatmap working) @@ -59,21 +56,14 @@ namespace osu.Game.Tests.Visual Clock.ProcessFrame(); } - protected override bool OnWheel(InputState state) + protected override bool OnScroll(InputState state) { - if (state.Mouse.WheelDelta > 0) + if (state.Mouse.ScrollDelta.Y > 0) Clock.SeekBackward(true); else Clock.SeekForward(true); return true; } - - protected override void Dispose(bool isDisposing) - { - osuGame.Beatmap.ValueChanged -= beatmapChanged; - - base.Dispose(isDisposing); - } } } diff --git a/osu.Game/Tests/Visual/EditorTestCase.cs b/osu.Game/Tests/Visual/EditorTestCase.cs index c0b72f56a4..2ab121fcc9 100644 --- a/osu.Game/Tests/Visual/EditorTestCase.cs +++ b/osu.Game/Tests/Visual/EditorTestCase.cs @@ -23,9 +23,9 @@ namespace osu.Game.Tests.Visual } [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) + private void load() { - osuGame.Beatmap.Value = new TestWorkingBeatmap(ruleset.RulesetInfo); + Beatmap.Value = new TestWorkingBeatmap(ruleset.RulesetInfo); LoadComponentAsync(new Editor(), LoadScreen); } diff --git a/osu.Game/Tests/Visual/OsuTestCase.cs b/osu.Game/Tests/Visual/OsuTestCase.cs index 2b677f1f42..1740658da6 100644 --- a/osu.Game/Tests/Visual/OsuTestCase.cs +++ b/osu.Game/Tests/Visual/OsuTestCase.cs @@ -1,20 +1,51 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.IO; -using System.Reflection; +using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Testing; +using osu.Game.Beatmaps; namespace osu.Game.Tests.Visual { public abstract class OsuTestCase : TestCase { + private readonly OsuTestBeatmap beatmap = new OsuTestBeatmap(new DummyWorkingBeatmap()); + protected BindableBeatmap Beatmap => beatmap; + + protected DependencyContainer Dependencies { get; private set; } + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + Dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + + Dependencies.CacheAs(beatmap); + Dependencies.CacheAs(beatmap); + + return Dependencies; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audioManager) + { + beatmap.SetAudioManager(audioManager); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (beatmap != null) + { + beatmap.Disabled = true; + beatmap.Value.Track.Stop(); + } + } + protected override ITestCaseTestRunner CreateRunner() => new OsuTestCaseTestRunner(); public class OsuTestCaseTestRunner : OsuGameBase, ITestCaseTestRunner { - protected override string MainResourceFile => File.Exists(base.MainResourceFile) ? base.MainResourceFile : Assembly.GetExecutingAssembly().Location; - private TestCaseTestRunner.TestRunner runner; protected override void LoadAsyncComplete() @@ -27,5 +58,22 @@ namespace osu.Game.Tests.Visual public void RunTestBlocking(TestCase test) => runner.RunTestBlocking(test); } + + private class OsuTestBeatmap : BindableBeatmap + { + public OsuTestBeatmap(WorkingBeatmap defaultValue) + : base(defaultValue) + { + } + + public void SetAudioManager(AudioManager audioManager) => RegisterAudioManager(audioManager); + + public override BindableBeatmap GetBoundCopy() + { + var copy = new OsuTestBeatmap(Default); + copy.BindTo(this); + return copy; + } + } } } diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs deleted file mode 100644 index 51460ecb6d..0000000000 --- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs +++ /dev/null @@ -1,400 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using System.Linq; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Caching; -using osu.Framework.Configuration; -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.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 == null || 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; - - private readonly Bindable currentBeatmap = new Bindable(); - - 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 OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "Please sign in to see online scores", - }; - } - - currentBeatmap.ValueChanged += beatmapChanged; - currentBeatmap.BindTo(osuGame.Beatmap); - } - - private GetScoresRequest lastRequest; - private void beatmapChanged(WorkingBeatmap newBeatmap) - { - if (!IsAlive) return; - - 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 IBeatmap beatmap; - - public PerformanceDisplay(Score score, IBeatmap 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 bda438d906..20c9646aa3 100644 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ b/osu.Game/Tests/Visual/TestCasePlayer.cs @@ -1,9 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; +using osu.Framework.Lists; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -19,8 +21,6 @@ namespace osu.Game.Tests.Visual protected Player Player; - private TestWorkingBeatmap working; - protected TestCasePlayer(Ruleset ruleset) { this.ruleset = ruleset; @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual { Player p = null; AddStep(ruleset.RulesetInfo.Name, () => p = loadPlayerFor(ruleset)); - AddUntilStep(() => p.IsLoaded); + AddUntilStep(() => ContinueCondition(p)); } else { @@ -52,28 +52,63 @@ namespace osu.Game.Tests.Visual { Player p = null; AddStep(r.Name, () => p = loadPlayerFor(r)); - AddUntilStep(() => p.IsLoaded); + AddUntilStep(() => ContinueCondition(p)); + + AddAssert("no leaked beatmaps", () => + { + p = null; + + GC.Collect(); + GC.WaitForPendingFinalizers(); + int count = 0; + + workingWeakReferences.ForEachAlive(_ => count++); + return count == 1; + }); + + AddAssert("no leaked players", () => + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + int count = 0; + + playerWeakReferences.ForEachAlive(_ => count++); + return count == 1; + }); } } } + protected virtual bool ContinueCondition(Player player) => player.IsLoaded; + protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo); + private readonly WeakList workingWeakReferences = new WeakList(); + private readonly WeakList playerWeakReferences = new WeakList(); + private Player loadPlayerFor(RulesetInfo ri) => loadPlayerFor(ri.CreateInstance()); private Player loadPlayerFor(Ruleset r) { var beatmap = CreateBeatmap(r); + var working = new TestWorkingBeatmap(beatmap); - working = new TestWorkingBeatmap(beatmap); - working.Mods.Value = new[] { r.GetAllMods().First(m => m is ModNoFail) }; + workingWeakReferences.Add(working); - if (Player != null) - Remove(Player); + Beatmap.Value = working; + Beatmap.Value.Mods.Value = new[] { r.GetAllMods().First(m => m is ModNoFail) }; - var player = CreatePlayer(working, r); + Player?.Exit(); - LoadComponentAsync(player, LoadScreen); + var player = CreatePlayer(r); + + playerWeakReferences.Add(player); + + LoadComponentAsync(player, p => + { + Player = p; + LoadScreen(p); + }); return player; } @@ -82,14 +117,12 @@ namespace osu.Game.Tests.Visual { base.Update(); - if (working != null) - // note that this will override any mod rate application - working.Track.Rate = Clock.Rate; + // note that this will override any mod rate application + Beatmap.Value.Track.Rate = Clock.Rate; } - protected virtual Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) => new Player + protected virtual Player CreatePlayer(Ruleset ruleset) => new Player { - InitialBeatmap = beatmap, AllowPause = false, AllowLeadIn = false, AllowResults = false, diff --git a/osu.Game/Utils/DebugUtils.cs b/osu.Game/Utils/DebugUtils.cs new file mode 100644 index 0000000000..191662c690 --- /dev/null +++ b/osu.Game/Utils/DebugUtils.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Utils +{ + public static class DebugUtils + { + public static bool IsDebug + { + get + { + // ReSharper disable once RedundantAssignment + bool isDebug = false; + // Debug.Assert conditions are only evaluated in debug mode + System.Diagnostics.Debug.Assert(isDebug = true); + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + return isDebug; + } + } + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1a75f1979a..f0bc330994 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -11,16 +11,16 @@ - - - - + + + + - + \ No newline at end of file diff --git a/osu.TestProject.props b/osu.TestProject.props index afdf895eac..8f7128f8b7 100644 --- a/osu.TestProject.props +++ b/osu.TestProject.props @@ -7,18 +7,17 @@ - - + - + - + VisualTestRunner.cs @@ -26,4 +25,4 @@ false - \ No newline at end of file + diff --git a/osu.sln b/osu.sln index 5c4b644489..bf1b6d60e1 100644 --- a/osu.sln +++ b/osu.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 15.0.27004.2006 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game", "osu.Game\osu.Game.csproj", "{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Framework", "osu-framework\osu.Framework\osu.Framework.csproj", "{C76BF5B3-985E-4D39-95FE-97C9C879B83A}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Resources", "osu-resources\osu.Game.Resources\osu.Game.Resources.csproj", "{D9A367C9-4C1A-489F-9B05-A0CEA2B53B58}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Osu", "osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj", "{C92A607B-1FDD-4954-9F92-03FF547D9080}" @@ -17,8 +15,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Taiko", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Mania", "osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj", "{48F4582B-7687-4621-9CBE-5C24197CB536}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Desktop.Deploy", "osu.Desktop.Deploy\osu.Desktop.Deploy.csproj", "{BAEA2F74-0315-4667-84E0-ACAC0B4BF785}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Tests", "osu.Game.Tests\osu.Game.Tests.csproj", "{54377672-20B1-40AF-8087-5CF73BF3953A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Desktop", "osu.Desktop\osu.Desktop.csproj", "{419659FD-72EA-4678-9EB8-B22A746CED70}" @@ -41,10 +37,6 @@ 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 - {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 - {C76BF5B3-985E-4D39-95FE-97C9C879B83A}.Release|Any CPU.Build.0 = Release|Any CPU {D9A367C9-4C1A-489F-9B05-A0CEA2B53B58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9A367C9-4C1A-489F-9B05-A0CEA2B53B58}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9A367C9-4C1A-489F-9B05-A0CEA2B53B58}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -65,8 +57,6 @@ Global {48F4582B-7687-4621-9CBE-5C24197CB536}.Debug|Any CPU.Build.0 = Debug|Any CPU {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|Any CPU.ActiveCfg = Release|Any CPU {48F4582B-7687-4621-9CBE-5C24197CB536}.Release|Any CPU.Build.0 = Release|Any CPU - {BAEA2F74-0315-4667-84E0-ACAC0B4BF785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BAEA2F74-0315-4667-84E0-ACAC0B4BF785}.Release|Any CPU.ActiveCfg = Release|Any CPU {54377672-20B1-40AF-8087-5CF73BF3953A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54377672-20B1-40AF-8087-5CF73BF3953A}.Debug|Any CPU.Build.0 = Debug|Any CPU {54377672-20B1-40AF-8087-5CF73BF3953A}.Release|Any CPU.ActiveCfg = Release|Any CPU