diff --git a/README.md b/README.md index a1932b0fdf..a1f478e39a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Clone the repository including submodules Build and run - Using Visual Studio 2017, Rider or Visual Studio Code (configurations are included) -- From command line using `dotnet run --project osu.Desktop --framework netcoreapp2.1` +- From command line using `dotnet run --project osu.Desktop` If you run into issues building you may need to restore nuget packages (commonly via `dotnet restore`). Visual Studio Code users must run `Restore` task from debug tab before attempt to build. diff --git a/appveyor.yml b/appveyor.yml index 7c08eb9e9c..8c06b42fd2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,12 +9,10 @@ install: - cmd: git submodule update --init --recursive --depth=5 - cmd: choco install resharper-clt -y - cmd: choco install nvika -y - - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.5/CodeFileSanity.exe + - cmd: dotnet tool install CodeFileSanity --version 0.0.16 --global before_build: - - cmd: CodeFileSanity.exe + - cmd: CodeFileSanity - cmd: nuget restore -verbosity quiet -environment: - TargetFramework: net471 build: project: osu.sln parallel: true diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml index 0247714cdf..6d8d95e773 100644 --- a/appveyor_deploy.yml +++ b/appveyor_deploy.yml @@ -16,17 +16,15 @@ build_script: - 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% + - cmd: ..\appveyor-tools\secure-file -decrypt ..\fdc6f19b04.enc -secret %decode_secret% -out bin\Debug\netcoreapp2.1\osu.Desktop.Deploy.dll.config + - dotnet bin/Debug/netcoreapp2.1/osu.Desktop.Deploy.dll %code_signing_password% %APPVEYOR_REPO_TAG_NAME% environment: - TargetFramework: net471 decode_secret: secure: i67IC2xj6DjjxmA6Oj2jing3+MwzLkq6CbGsjfZ7rdY= code_signing_password: secure: 34tLNqvjmmZEi97MLKfrnQ== artifacts: - - path: 'Releases\*' + - path: 'osu-deploy/releases/*' deploy: - provider: Environment name: github \ No newline at end of file diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index a4270f22b4..79ac24a1da 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -13,6 +13,7 @@ using osu.Game; using OpenTK.Input; using Microsoft.Win32; using osu.Desktop.Updater; +using osu.Framework; using osu.Framework.Platform.Windows; namespace osu.Desktop @@ -51,11 +52,10 @@ namespace osu.Desktop v.State = Visibility.Visible; }); -#if NET_FRAMEWORK - Add(new SquirrelUpdateManager()); -#else - Add(new SimpleUpdateManager()); -#endif + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + Add(new SquirrelUpdateManager()); + else + Add(new SimpleUpdateManager()); } } diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 7e6a07c89f..257155478f 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -4,14 +4,14 @@ using System; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework; +using osu.Framework.Development; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IPC; -#if NET_FRAMEWORK -using System.Runtime; -#endif - namespace osu.Desktop { public static class Program @@ -19,13 +19,13 @@ namespace osu.Desktop [STAThread] public static int Main(string[] args) { - useMultiCoreJit(); - // Back up the cwd before DesktopGameHost changes it var cwd = Environment.CurrentDirectory; using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) { + host.ExceptionThrown += handleException; + if (!host.IsPrimaryInstance) { var importer = new ArchiveImportIPCChannel(host); @@ -52,13 +52,23 @@ namespace osu.Desktop } } - private static void useMultiCoreJit() + private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1; + + /// + /// Allow a maximum of one unhandled exception, per second of execution. + /// + /// + /// + private static bool handleException(Exception arg) { -#if NET_FRAMEWORK - var directory = Directory.CreateDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Profiles")); - ProfileOptimization.SetProfileRoot(directory.FullName); - ProfileOptimization.StartProfile("Startup.Profile"); -#endif + bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0; + + Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} ."); + + // restore the stock of allowable exceptions after a short delay. + Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions)); + + return continueExecution; } } } diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 81da26cff2..1f8bff74f4 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.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 -#if NET_FRAMEWORK using System; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -162,4 +161,3 @@ namespace osu.Desktop.Updater } } } -#endif diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 0289db20f0..180abd7fec 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -1,7 +1,7 @@  - net471;netcoreapp2.1 + netcoreapp2.1 WinExe AnyCPU true @@ -13,9 +13,6 @@ 0.0.0 0.0.0 - - $(DefineConstants);NET_FRAMEWORK - osu.Desktop.Program @@ -29,11 +26,12 @@ + + - - \ No newline at end of file + diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec index 316a5443ef..cdd232a9b2 100644 --- a/osu.Desktop/osu.nuspec +++ b/osu.Desktop/osu.nuspec @@ -3,7 +3,7 @@ osulazer 0.0.0 - osulazer + osu!lazer ppy Pty Ltd Dean Herbert https://osu.ppy.sh/ diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json index 2a82d65014..da9344b6a2 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/launch.json @@ -2,35 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "VisualTests (Debug, net471)", - "windows": { - "type": "clr" - }, - "type": "mono", - "request": "launch", - "program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Catch.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}/bin/Release/net471/osu.Game.Rulesets.Catch.Tests.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, msbuild)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - }, - { - "name": "VisualTests (Debug, netcoreapp2.1)", + "name": "VisualTests (Debug)", "type": "coreclr", "request": "launch", "program": "dotnet", @@ -38,12 +10,12 @@ "${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Catch.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Debug, dotnet)", + "preLaunchTask": "Build (Debug)", "env": {}, "console": "internalConsole" }, { - "name": "VisualTests (Release, netcoreapp2.1)", + "name": "VisualTests (Release)", "type": "coreclr", "request": "launch", "program": "dotnet", @@ -51,7 +23,7 @@ "${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Catch.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, dotnet)", + "preLaunchTask": "Build (Release)", "env": {}, "console": "internalConsole" } diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json index 6c6d562512..18a6f8ca70 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json @@ -4,43 +4,13 @@ "version": "2.0.0", "tasks": [ { - "label": "Build (Debug, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "osu.Game.Rulesets.Catch.Tests.csproj", - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Release, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "osu.Game.Rulesets.Catch.Tests.csproj", - "/p:Configuration=Release", - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Debug, dotnet)", + "label": "Build (Debug)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", - "/p:TargetFramework=netcoreapp2.1", "/p:GenerateFullPaths=true", "/m", "/verbosity:m" @@ -49,14 +19,13 @@ "problemMatcher": "$msCompile" }, { - "label": "Build (Release, dotnet)", + "label": "Build (Release)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", - "/p:TargetFramework=netcoreapp2.1", "/p:Configuration=Release", "/p:GenerateFullPaths=true", "/m", @@ -66,16 +35,7 @@ "problemMatcher": "$msCompile" }, { - "label": "Restore (net471)", - "type": "shell", - "command": "nuget", - "args": [ - "restore" - ], - "problemMatcher": [] - }, - { - "label": "Restore (netcoreapp2.1)", + "label": "Restore", "type": "shell", "command": "dotnet", "args": [ 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 93fa2c4d67..51343d9e91 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 @@ -1,8 +1,14 @@  + + + + + + WinExe - netcoreapp2.1;net471 + netcoreapp2.1 diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 68a8dfb7d3..15d4edc411 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps RepeatCount = curveData.RepeatCount, X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH, NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0 }; } @@ -51,7 +52,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps StartTime = obj.StartTime, Samples = obj.Samples, Duration = endTime.Duration, - NewCombo = comboData?.NewCombo ?? false + NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, }; } else @@ -61,6 +63,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps StartTime = obj.StartTime, Samples = obj.Samples, NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH }; } diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index fc6e23c884..1f1d2475f6 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -93,13 +93,11 @@ namespace osu.Game.Rulesets.Catch new CatchModHidden(), new CatchModFlashlight(), }; - case ModType.Special: + case ModType.Automation: return new Mod[] { - new CatchModRelax(), - null, - null, new MultiMod(new CatchModAutoplay(), new ModCinema()), + new CatchModRelax(), }; default: return new Mod[] { }; diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs index c39e663d75..f38009263f 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.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.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Judgements @@ -9,8 +10,6 @@ namespace osu.Game.Rulesets.Catch.Judgements { public override bool AffectsCombo => false; - public override bool ShouldExplode => true; - protected override int NumericResultFor(HitResult result) { switch (result) @@ -32,5 +31,7 @@ namespace osu.Game.Rulesets.Catch.Judgements return 8; } } + + public override bool ShouldExplodeFor(JudgementResult result) => true; } } diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs index 51d7d3b5cd..8a51867899 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs @@ -23,21 +23,10 @@ namespace osu.Game.Rulesets.Catch.Judgements } /// - /// The base health increase for the result achieved. + /// Retrieves the numeric health increase of a . /// - public float HealthIncrease => HealthIncreaseFor(Result); - - /// - /// Whether fruit on the platter should explode or drop. - /// Note that this is only checked if the owning object is also - /// - public virtual bool ShouldExplode => IsHit; - - /// - /// Convert a to a base health increase. - /// - /// The value to convert. - /// The base health increase. + /// The to find the numeric health increase for. + /// The numeric health increase of . protected virtual float HealthIncreaseFor(HitResult result) { switch (result) @@ -48,5 +37,18 @@ namespace osu.Game.Rulesets.Catch.Judgements return 10.2f; } } + + /// + /// Retrieves the numeric health increase of a . + /// + /// The to find the numeric health increase for. + /// The numeric health increase of . + public float HealthIncreaseFor(JudgementResult result) => HealthIncreaseFor(result.Type); + + /// + /// Whether fruit on the platter should explode or drop. + /// Note that this is only checked if the owning object is also + /// + public virtual bool ShouldExplodeFor(JudgementResult result) => result.IsHit; } } diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index f7c60a7a47..e1af4c1075 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -1,10 +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.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Catch.Objects { public class Banana : Fruit { public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; + + public override Judgement CreateJudgement() => new CatchBananaJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index d55cdac115..621fc100c2 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects public virtual bool NewCombo { get; set; } + public int ComboOffset { get; set; } + public int IndexInCurrentCombo { get; set; } public int ComboIndex { get; set; } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs index dd027abbe0..8756a5178f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBanana.cs @@ -1,8 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Rulesets.Catch.Judgements; - namespace osu.Game.Rulesets.Catch.Objects.Drawable { public class DrawableBanana : DrawableFruit @@ -11,7 +9,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable : base(h) { } - - protected override CatchJudgement CreateJudgement() => new CatchBananaJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs index f039504600..697fab85c9 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs @@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable AddNested(getVisualRepresentation?.Invoke(b)); } - protected override bool ProvidesJudgement => false; - protected override void AddNested(DrawableHitObject h) { ((DrawableCatchHitObject)h).CheckPosition = o => CheckPosition?.Invoke(o) ?? false; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index 6ce2e6a2ae..9e840301fd 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -5,7 +5,6 @@ using System; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -53,20 +52,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable public Func CheckPosition; - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (CheckPosition == null) return; - if (timeOffset >= 0) - { - var judgement = CreateJudgement(); - judgement.Result = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss; - AddJudgement(judgement); - } + if (timeOffset >= 0 && Result != null) + ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss); } - protected virtual CatchJudgement CreateJudgement() => new CatchJudgement(); - protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs index 11d5ed1f92..5c8a7c4a7c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces; using OpenTK; using OpenTK.Graphics; -using osu.Game.Rulesets.Catch.Judgements; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -24,8 +23,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Masking = false; } - protected override CatchJudgement CreateJudgement() => new CatchDropletJudgement(); - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs index 854b63edeb..e66852c5c2 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs @@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable AddNested(getVisualRepresentation?.Invoke(o)); } - protected override bool ProvidesJudgement => false; - protected override void AddNested(DrawableHitObject h) { var catchObject = (DrawableCatchHitObject)h; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs index 2232bb81a7..e0f02454c4 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableTinyDroplet.cs @@ -2,18 +2,15 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using OpenTK; -using osu.Game.Rulesets.Catch.Judgements; namespace osu.Game.Rulesets.Catch.Objects.Drawable { public class DrawableTinyDroplet : DrawableDroplet { - public DrawableTinyDroplet(Droplet h) + public DrawableTinyDroplet(TinyDroplet h) : base(h) { Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS) / 8; } - - protected override CatchJudgement CreateJudgement() => new CatchTinyDropletJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs index f91a70c506..8b54922959 100644 --- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs @@ -1,9 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Catch.Objects { public class Droplet : CatchHitObject { + public override Judgement CreateJudgement() => new CatchDropletJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs index fcbb339ffd..2c2cd013c3 100644 --- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs @@ -1,9 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Catch.Objects { public class Fruit : CatchHitObject { + public override Judgement CreateJudgement() => new CatchJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs index 76cc8d9808..39f1cadad5 100644 --- a/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/TinyDroplet.cs @@ -1,9 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Catch.Objects { public class TinyDroplet : Droplet { + public override Judgement CreateJudgement() => new CatchTinyDropletJudgement(); } } diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 5b69d836a3..403cedde8c 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -2,7 +2,6 @@ // 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.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; @@ -21,55 +20,28 @@ namespace osu.Game.Rulesets.Catch.Scoring private float hpDrainRate; - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void ApplyBeatmap(Beatmap beatmap) { - hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; + base.ApplyBeatmap(beatmap); - foreach (var obj in beatmap.HitObjects) - { - switch (obj) - { - case JuiceStream stream: - foreach (var nestedObject in stream.NestedHitObjects) - switch (nestedObject) - { - case TinyDroplet _: - AddJudgement(new CatchTinyDropletJudgement { Result = HitResult.Perfect }); - break; - case Droplet _: - AddJudgement(new CatchDropletJudgement { Result = HitResult.Perfect }); - break; - case Fruit _: - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); - break; - } - break; - case BananaShower shower: - foreach (var _ in shower.NestedHitObjects.Cast()) - AddJudgement(new CatchBananaJudgement { Result = HitResult.Perfect }); - break; - case Fruit _: - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); - break; - } - } + hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; } private const double harshness = 0.01; - protected override void OnNewJudgement(Judgement judgement) + protected override void ApplyResult(JudgementResult result) { - base.OnNewJudgement(judgement); + base.ApplyResult(result); - if (judgement.Result == HitResult.Miss) + if (result.Type == HitResult.Miss) { - if (!judgement.IsBonus) + if (!result.Judgement.IsBonus) Health.Value -= hpDrainRate * (harshness * 2); return; } - if (judgement is CatchJudgement catchJudgement) - Health.Value += Math.Max(catchJudgement.HealthIncrease - hpDrainRate, 0) * harshness; + if (result.Judgement is CatchJudgement catchJudgement) + Health.Value += Math.Max(catchJudgement.HealthIncreaseFor(result) - hpDrainRate, 0) * harshness; } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index ea3b6fb0e0..d49be69856 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.UI public override void Add(DrawableHitObject h) { - h.OnJudgement += onJudgement; + h.OnNewResult += onNewResult; base.Add(h); @@ -67,6 +67,7 @@ namespace osu.Game.Rulesets.Catch.UI fruit.CheckPosition = CheckIfWeCanCatch; } - private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) => catcherArea.OnJudgement((DrawableCatchHitObject)judgedObject, judgement); + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) + => catcherArea.OnResult((DrawableCatchHitObject)judgedObject, result); } } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 7b06426b07..4327abb96f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.UI private DrawableCatchHitObject lastPlateableFruit; - public void OnJudgement(DrawableCatchHitObject fruit, Judgement judgement) + public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) { void runAfterLoaded(Action action) { @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.UI lastPlateableFruit.OnLoadComplete = _ => action(); } - if (judgement.IsHit && fruit.CanBePlated) + if (result.IsHit && fruit.CanBePlated) { var caughtFruit = (DrawableCatchHitObject)GetVisualRepresentation?.Invoke(fruit.HitObject); @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.UI if (fruit.HitObject.LastInCombo) { - if (((CatchJudgement)judgement).ShouldExplode) + if (((CatchJudgement)result.Judgement).ShouldExplodeFor(result)) runAfterLoaded(() => MovableCatcher.Explode()); else MovableCatcher.Drop(); diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json index bc41d4ccf9..c781b0e64e 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/launch.json @@ -2,35 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "VisualTests (Debug, net471)", - "windows": { - "type": "clr" - }, - "type": "mono", - "request": "launch", - "program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Mania.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}/bin/Release/net471/osu.Game.Rulesets.Mania.Tests.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, msbuild)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - }, - { - "name": "VisualTests (Debug, netcoreapp2.1)", + "name": "VisualTests (Debug)", "type": "coreclr", "request": "launch", "program": "dotnet", @@ -38,12 +10,12 @@ "${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Mania.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Debug, dotnet)", + "preLaunchTask": "Build (Debug)", "env": {}, "console": "internalConsole" }, { - "name": "VisualTests (Release, netcoreapp2.1)", + "name": "VisualTests (Release)", "type": "coreclr", "request": "launch", "program": "dotnet", @@ -51,7 +23,7 @@ "${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Mania.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, dotnet)", + "preLaunchTask": "Build (Release)", "env": {}, "console": "internalConsole" } diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json index 7fc2f7b2ef..608c4340ac 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json @@ -4,43 +4,13 @@ "version": "2.0.0", "tasks": [ { - "label": "Build (Debug, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "osu.Game.Rulesets.Mania.Tests.csproj", - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Release, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "osu.Game.Rulesets.Mania.Tests.csproj", - "/p:Configuration=Release", - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Debug, dotnet)", + "label": "Build (Debug)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", - "/p:TargetFramework=netcoreapp2.1", "/p:GenerateFullPaths=true", "/m", "/verbosity:m" @@ -49,14 +19,13 @@ "problemMatcher": "$msCompile" }, { - "label": "Build (Release, dotnet)", + "label": "Build (Release)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", - "/p:TargetFramework=netcoreapp2.1", "/p:Configuration=Release", "/p:GenerateFullPaths=true", "/m", @@ -66,16 +35,7 @@ "problemMatcher": "$msCompile" }, { - "label": "Restore (net471)", - "type": "shell", - "command": "nuget", - "args": [ - "restore" - ], - "problemMatcher": [] - }, - { - "label": "Restore (netcoreapp2.1)", + "label": "Restore", "type": "shell", "command": "dotnet", "args": [ diff --git a/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs index 5a93efb0dc..29663c2093 100644 --- a/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs @@ -14,24 +14,20 @@ namespace osu.Game.Rulesets.Mania.Tests /// public class ScrollingTestContainer : Container { - private readonly ScrollingDirection direction; + [Cached(Type = typeof(IScrollingInfo))] + private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); public ScrollingTestContainer(ScrollingDirection direction) { - this.direction = direction; + scrollingInfo.Direction.Value = direction; } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new ScrollingInfo { Direction = { Value = direction }}); - return dependencies; - } + public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up; + } - private class ScrollingInfo : IScrollingInfo - { - public readonly Bindable Direction = new Bindable(); - IBindable IScrollingInfo.Direction => Direction; - } + public class TestScrollingInfo : IScrollingInfo + { + public readonly Bindable Direction = new Bindable(); + IBindable IScrollingInfo.Direction => Direction; } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseEditor.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseEditor.cs new file mode 100644 index 0000000000..6c0f931cda --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseEditor.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [TestFixture] + public class TestCaseEditor : EditorTestCase + { + private readonly Bindable direction = new Bindable(); + + public TestCaseEditor() + : base(new ManiaRuleset()) + { + AddStep("upwards scroll", () => direction.Value = ManiaScrollingDirection.Up); + AddStep("downwards scroll", () => direction.Value = ManiaScrollingDirection.Down); + } + + [BackgroundDependencyLoader] + private void load(RulesetConfigCache configCache) + { + var config = (ManiaConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance()); + config.BindWith(ManiaSetting.ScrollDirection, direction); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs index fdc8f362f7..a8b2b20fda 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseNotes.cs @@ -46,15 +46,20 @@ namespace osu.Game.Rulesets.Mania.Tests Spacing = new Vector2(20), Children = new[] { - createNoteDisplay(ScrollingDirection.Down), - createNoteDisplay(ScrollingDirection.Up), - createHoldNoteDisplay(ScrollingDirection.Down), - createHoldNoteDisplay(ScrollingDirection.Up), + createNoteDisplay(ScrollingDirection.Down, 1, out var note1), + createNoteDisplay(ScrollingDirection.Up, 2, out var note2), + createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1), + createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2), } }; + + AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2)); + AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0)); + AddAssert("hold note 1 facing downwards", () => verifyAnchors(holdNote1, Anchor.y2)); + AddAssert("hold note 2 facing upwards", () => verifyAnchors(holdNote2, Anchor.y0)); } - private Drawable createNoteDisplay(ScrollingDirection direction) + private Drawable createNoteDisplay(ScrollingDirection direction, int identifier, out DrawableNote hitObject) { var note = new Note { StartTime = 999999999 }; note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -62,24 +67,24 @@ namespace osu.Game.Rulesets.Mania.Tests return new ScrollingTestContainer(direction) { AutoSizeAxes = Axes.Both, - Child = new NoteContainer(direction, $"note, scrolling {direction.ToString().ToLowerInvariant()}") + Child = new NoteContainer(direction, $"note {identifier}, scrolling {direction.ToString().ToLowerInvariant()}") { - Child = new DrawableNote(note) { AccentColour = Color4.OrangeRed } + Child = hitObject = new DrawableNote(note) { AccentColour = Color4.OrangeRed } } }; } - private Drawable createHoldNoteDisplay(ScrollingDirection direction) + private Drawable createHoldNoteDisplay(ScrollingDirection direction, int identifier, out DrawableHoldNote hitObject) { - var note = new HoldNote { StartTime = 999999999, Duration = 1000 }; + var note = new HoldNote { StartTime = 999999999, Duration = 5000 }; note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); return new ScrollingTestContainer(direction) { AutoSizeAxes = Axes.Both, - Child = new NoteContainer(direction, $"hold note, scrolling {direction.ToString().ToLowerInvariant()}") + Child = new NoteContainer(direction, $"hold note {identifier}, scrolling {direction.ToString().ToLowerInvariant()}") { - Child = new DrawableHoldNote(note) + Child = hitObject = new DrawableHoldNote(note) { RelativeSizeAxes = Axes.Both, AccentColour = Color4.OrangeRed, @@ -88,6 +93,12 @@ namespace osu.Game.Rulesets.Mania.Tests }; } + private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor) + => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor); + + private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor) + => verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor)); + private class NoteContainer : Container { private readonly Container content; diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs index b1bb7f5187..5c5d955168 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs @@ -2,6 +2,7 @@ // 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.Framework.Graphics; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Mania.Tests private readonly List stages = new List(); + private FillFlowContainer fill; + public TestCaseStage() : base(columns) { @@ -32,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests [BackgroundDependencyLoader] private void load() { - Child = new FillFlowContainer + Child = fill = new FillFlowContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -54,8 +57,22 @@ namespace osu.Game.Rulesets.Mania.Tests AddStep("hold note", createHoldNote); AddStep("minor bar line", () => createBarLine(false)); AddStep("major bar line", () => createBarLine(true)); + + AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.TopCentre)); + AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.BottomCentre)); + + AddStep("flip direction", () => + { + foreach (var c in fill.Children) + c.Flip(); + }); + + AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.BottomCentre)); + AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.TopCentre)); } + private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor); + private void createNote() { foreach (var stage in stages) @@ -101,7 +118,7 @@ namespace osu.Game.Rulesets.Mania.Tests } } - private Drawable createStage(ScrollingDirection direction, ManiaAction action) + private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action) { var specialAction = ManiaAction.Special1; 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 77504fdc3c..3165f69a6b 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 @@ -1,8 +1,14 @@  + + + + + + WinExe - netcoreapp2.1;net471 + netcoreapp2.1 diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 8d0d78120a..010cf962cc 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -176,16 +176,23 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = Random.Next(RandomStart, TotalColumns); for (int i = 0; i < Math.Min(usableColumns, noteCount); i++) { - while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn)) //find available column + // Find available column + RunWhile(() => pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn), () => + { nextColumn = Random.Next(RandomStart, TotalColumns); + }); + addToPattern(pattern, nextColumn, startTime, EndTime); } // This is can't be combined with the above loop due to RNG for (int i = 0; i < noteCount - usableColumns; i++) { - while (pattern.ColumnHasObject(nextColumn)) + RunWhile(() => pattern.ColumnHasObject(nextColumn), () => + { nextColumn = Random.Next(RandomStart, TotalColumns); + }); + addToPattern(pattern, nextColumn, startTime, EndTime); } @@ -211,16 +218,21 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) { - while (PreviousPattern.ColumnHasObject(nextColumn)) + RunWhile(() => PreviousPattern.ColumnHasObject(nextColumn), () => + { nextColumn = Random.Next(RandomStart, TotalColumns); + }); } int lastColumn = nextColumn; for (int i = 0; i < noteCount; i++) { addToPattern(pattern, nextColumn, startTime, startTime); - while (nextColumn == lastColumn) + + RunWhile(() => nextColumn == lastColumn, () => + { nextColumn = Random.Next(RandomStart, TotalColumns); + }); lastColumn = nextColumn; startTime += SegmentDuration; @@ -393,14 +405,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) { - while (PreviousPattern.ColumnHasObject(nextColumn)) + RunWhile(() => PreviousPattern.ColumnHasObject(nextColumn), () => + { nextColumn = Random.Next(RandomStart, TotalColumns); + }); } for (int i = 0; i < columnRepeat; i++) { - while (pattern.ColumnHasObject(nextColumn)) + RunWhile(() => pattern.ColumnHasObject(nextColumn), () => + { nextColumn = Random.Next(RandomStart, TotalColumns); + }); addToPattern(pattern, nextColumn, startTime, EndTime); startTime += SegmentDuration; @@ -427,8 +443,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) { - while (PreviousPattern.ColumnHasObject(holdColumn)) + RunWhile(() => PreviousPattern.ColumnHasObject(holdColumn), () => + { holdColumn = Random.Next(RandomStart, TotalColumns); + }); } // Create the hold note @@ -455,8 +473,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { for (int j = 0; j < noteCount; j++) { - while (rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn) + RunWhile(() => rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn, () => + { nextColumn = Random.Next(RandomStart, TotalColumns); + }); + addToPattern(rowPattern, nextColumn, startTime, startTime); } } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 06b4b8a27e..eae9a0fc3b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -59,8 +59,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { int nextColumn = Random.Next(start, TotalColumns); - while (PreviousPattern.ColumnHasObject(nextColumn)) + RunWhile(() => PreviousPattern.ColumnHasObject(nextColumn), () => + { nextColumn = Random.Next(start, TotalColumns); + }); return nextColumn; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 84ebfdb839..5860480a91 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -234,7 +234,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); for (int i = 0; i < noteCount; i++) { - while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking) + RunWhile(() => pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking, () => { if (convertType.HasFlag(PatternType.Gathered)) { @@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } else nextColumn = Random.Next(RandomStart, TotalColumns); - } + }); addToPattern(pattern, nextColumn); } @@ -286,6 +286,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The containing the hit objects. private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) { + if (convertType.HasFlag(PatternType.ForceNotStack)) + return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); + var pattern = new Pattern(); bool addToCentre; @@ -295,8 +298,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = Random.Next(RandomStart, columnLimit); for (int i = 0; i < noteCount; i++) { - while (pattern.ColumnHasObject(nextColumn)) + RunWhile(() => pattern.ColumnHasObject(nextColumn), () => + { nextColumn = Random.Next(RandomStart, columnLimit); + }); // Add normal note addToPattern(pattern, nextColumn); @@ -368,9 +373,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { addToCentre = false; - if (convertType.HasFlag(PatternType.ForceNotStack)) - return getRandomNoteCount(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); - switch (TotalColumns) { case 2: diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs index a42d57cdd1..e51cbcdc60 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; +using osu.Framework.Logging; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns @@ -12,6 +15,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// internal abstract class PatternGenerator { + /// + /// An arbitrary maximum amount of iterations to perform in . + /// The specific value is not super important - enough such that no false-positives occur. + /// + /// /b/933228 requires at least 23 iterations. + /// + private const int max_rng_iterations = 30; + /// /// The last pattern. /// @@ -42,10 +53,44 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns TotalColumns = Beatmap.TotalColumns; } + protected void RunWhile([InstantHandle] Func condition, Action action) + { + int iterations = 0; + + while (condition()) + { + if (iterations++ >= max_rng_iterations) + { + // log an error but don't throw. we want to continue execution. + Logger.Error(new ExceededAllowedIterationsException(new StackTrace(0)), + "Conversion encountered errors. The beatmap may not be correctly converted."); + return; + } + + action(); + } + } + /// /// Generates the patterns for , each filled with hit objects. /// /// The s containing the hit objects. public abstract IEnumerable Generate(); + + /// + /// Denotes when a single conversion operation is in an infinitely looping state. + /// + public class ExceededAllowedIterationsException : Exception + { + private readonly string stackTrace; + + public ExceededAllowedIterationsException(StackTrace stackTrace) + { + this.stackTrace = stackTrace.ToString(); + } + + public override string StackTrace => stackTrace; + public override string ToString() => $"{GetType().Name}: {Message}\r\n{StackTrace}"; + } } } diff --git a/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/HoldNoteMask.cs b/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/HoldNoteMask.cs new file mode 100644 index 0000000000..bfa6bc0a17 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/HoldNoteMask.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 osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays +{ + public class HoldNoteMask : HitObjectMask + { + public new DrawableHoldNote HitObject => (DrawableHoldNote)base.HitObject; + + private readonly IBindable direction = new Bindable(); + + private readonly BodyPiece body; + + public HoldNoteMask(DrawableHoldNote hold) + : base(hold) + { + InternalChildren = new Drawable[] + { + new HoldNoteNoteMask(hold.Head), + new HoldNoteNoteMask(hold.Tail), + body = new BodyPiece + { + AccentColour = Color4.Transparent + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, IScrollingInfo scrollingInfo) + { + body.BorderColour = colours.Yellow; + + direction.BindTo(scrollingInfo.Direction); + } + + protected override void Update() + { + base.Update(); + + Size = HitObject.DrawSize + new Vector2(0, HitObject.Tail.DrawHeight); + Position = Parent.ToLocalSpace(HitObject.ScreenSpaceDrawQuad.TopLeft); + + // This is a side-effect of not matching the hitobject's anchors/origins, which is kinda hard to do + // When scrolling upwards our origin is already at the top of the head note (which is the intended location), + // but when scrolling downwards our origin is at the _bottom_ of the tail note (where we need to be at the _top_ of the tail note) + if (direction.Value == ScrollingDirection.Down) + Y -= HitObject.Tail.DrawHeight; + } + + private class HoldNoteNoteMask : NoteMask + { + public HoldNoteNoteMask(DrawableNote note) + : base(note) + { + Select(); + } + + protected override void Update() + { + base.Update(); + + Anchor = HitObject.Anchor; + Origin = HitObject.Origin; + + Position = HitObject.DrawPosition; + } + + // Todo: This is temporary, since the note masks don't do anything special yet. In the future they will handle input. + public override bool HandleMouseInput => false; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/NoteMask.cs b/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/NoteMask.cs new file mode 100644 index 0000000000..78f876cb14 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/NoteMask.cs @@ -0,0 +1,39 @@ +// 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.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; + +namespace osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays +{ + public class NoteMask : HitObjectMask + { + public NoteMask(DrawableNote note) + : base(note) + { + Scale = note.Scale; + + CornerRadius = 5; + Masking = true; + + AddInternal(new NotePiece()); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Yellow; + } + + protected override void Update() + { + base.Update(); + + Size = HitObject.DrawSize; + Position = Parent.ToLocalSpace(HitObject.ScreenSpaceDrawQuad.TopLeft); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditPlayfield.cs b/osu.Game.Rulesets.Mania/Edit/ManiaEditPlayfield.cs new file mode 100644 index 0000000000..e7bc526471 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaEditPlayfield.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.UI; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaEditPlayfield : ManiaPlayfield + { + public ManiaEditPlayfield(List stages) + : base(stages) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs new file mode 100644 index 0000000000..a01947a60b --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.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.Framework.Graphics; +using OpenTK; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaEditRulesetContainer : ManiaRulesetContainer + { + public ManiaEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) + : base(ruleset, beatmap) + { + } + + protected override Playfield CreatePlayfield() => new ManiaEditPlayfield(Beatmap.Stages) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + protected override Vector2 PlayfieldArea => Vector2.One; + } +} diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs new file mode 100644 index 0000000000..f37d8134ce --- /dev/null +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.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 osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.Mania.UI; + +namespace osu.Game.Rulesets.Mania.Edit +{ + public class ManiaHitObjectComposer : HitObjectComposer + { + protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config; + + public ManiaHitObjectComposer(Ruleset ruleset) + : base(ruleset) + { + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(new ManiaScrollingInfo(Config)); + return dependencies; + } + + protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new ManiaEditRulesetContainer(ruleset, beatmap); + + protected override IReadOnlyList CompositionTools => new ICompositionTool[] + { + new HitObjectCompositionTool("Note"), + new HitObjectCompositionTool("Hold"), + }; + + public override HitObjectMask CreateMaskFor(DrawableHitObject hitObject) + { + switch (hitObject) + { + case DrawableNote note: + return new NoteMask(note); + case DrawableHoldNote holdNote: + return new HoldNoteMask(holdNote); + } + + return base.CreateMaskFor(hitObject); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs deleted file mode 100644 index 3a4beda77d..0000000000 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTailJudgement.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Mania.Judgements -{ - public class HoldNoteTailJudgement : ManiaJudgement - { - /// - /// Whether the hold note has been released too early and shouldn't give full score for the release. - /// - public bool HasBroken; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return base.NumericResultFor(result); - case HitResult.Great: - case HitResult.Perfect: - return base.NumericResultFor(HasBroken ? HitResult.Good : result); - } - } - } -} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 3d58e63da5..19e89c8ec5 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -19,9 +19,11 @@ using osu.Game.Configuration; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; +using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania @@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.Mania public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap); public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score); + public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); + public override IEnumerable ConvertLegacyMods(LegacyMods mods) { if (mods.HasFlag(LegacyMods.Nightcore)) @@ -120,7 +124,7 @@ namespace osu.Game.Rulesets.Mania new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()), new ManiaModFlashlight(), }; - case ModType.Special: + case ModType.Conversion: return new Mod[] { new MultiMod(new ManiaModKey4(), @@ -135,6 +139,10 @@ namespace osu.Game.Rulesets.Mania new ManiaModRandom(), new ManiaModDualStages(), new ManiaModMirror(), + }; + case ModType.Automation: + return new Mod[] + { new MultiMod(new ManiaModAutoplay(), new ModCinema()), }; default: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs index 6bfe295c3d..6d91c03c13 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public override string ShortenedName => Name; public abstract int KeyCount { get; } + public override ModType Type => ModType.Conversion; public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier public override bool Ranked => true; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs index 256811b4c1..aecfb50fbe 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs @@ -16,6 +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 ModType Type => ModType.Conversion; public override double ScoreMultiplier => 1; private bool isForCurrentRuleset; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs index 4192ec78da..847b0037f1 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public override string Name => "Mirror"; public override string ShortenedName => "MR"; - public override ModType Type => ModType.Special; + public override ModType Type => ModType.Conversion; public override double ScoreMultiplier => 1; public override bool Ranked => true; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs index 2f951461c3..0915b80742 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public override string Name => "Random"; public override string ShortenedName => "RD"; + public override ModType Type => ModType.Conversion; public override FontAwesome Icon => FontAwesome.fa_osu_dice; public override string Description => @"Shuffle around the keys!"; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 597450f223..af2a889f03 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using OpenTK.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Judgements; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; @@ -19,10 +18,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableHoldNote : DrawableManiaHitObject, IKeyBindingHandler { - public override bool DisplayJudgement => false; + public override bool DisplayResult => false; - private readonly DrawableNote head; - private readonly DrawableNote tail; + public readonly DrawableNote Head; + public readonly DrawableNote Tail; private readonly BodyPiece bodyPiece; @@ -57,12 +56,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables HoldStartTime = () => holdStartTime }) }, - head = new DrawableHeadNote(this) + Head = new DrawableHeadNote(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, - tail = new DrawableTailNote(this) + Tail = new DrawableTailNote(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre @@ -72,8 +71,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables foreach (var tick in tickContainer) AddNested(tick); - AddNested(head); - AddNested(tail); + AddNested(Head); + AddNested(Tail); } protected override void OnDirectionChanged(ScrollingDirection direction) @@ -91,16 +90,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.AccentColour = value; bodyPiece.AccentColour = value; - head.AccentColour = value; - tail.AccentColour = value; + Head.AccentColour = value; + Tail.AccentColour = value; tickContainer.ForEach(t => t.AccentColour = value); } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (tail.AllJudged) - AddJudgement(new HoldNoteJudgement { Result = HitResult.Perfect }); + if (Tail.AllJudged) + ApplyResult(r => r.Type = HitResult.Perfect); } protected override void Update() @@ -108,8 +107,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.Update(); // Make the body piece not lie under the head note - bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * head.Height / 2; - bodyPiece.Height = DrawHeight - head.Height / 2 + tail.Height / 2; + bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; + bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; } public bool OnPressed(ManiaAction action) @@ -141,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables holdStartTime = null; // If the key has been released too early, the user should not receive full score for the release - if (!tail.IsHit) + if (!Tail.IsHit) hasBroken = true; return true; @@ -166,7 +165,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return false; // If the key has been released too early, the user should not receive full score for the release - if (Judgements.Any(j => j.Result == HitResult.Miss)) + if (Result.Type == HitResult.Miss) holdNote.hasBroken = true; // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held @@ -197,7 +196,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables this.holdNote = holdNote; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { // Factor in the release lenience timeOffset /= release_window_lenience; @@ -205,13 +204,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - { - AddJudgement(new HoldNoteTailJudgement - { - Result = HitResult.Miss, - HasBroken = holdNote.hasBroken - }); - } + ApplyResult(r => r.Type = HitResult.Miss); return; } @@ -220,10 +213,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (result == HitResult.None) return; - AddJudgement(new HoldNoteTailJudgement + ApplyResult(r => { - Result = result, - HasBroken = holdNote.hasBroken + if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect)) + result = HitResult.Good; + + r.Type = result; }); } @@ -238,7 +233,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; - UpdateJudgement(true); + UpdateResult(true); // Handled by the hold note, which will set holding = false return false; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index 5df6079efa..01d5bc6fd4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -7,7 +7,6 @@ using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Judgements; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Scoring; @@ -72,29 +71,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!userTriggered) - return; - if (Time.Current < HitObject.StartTime) return; - if (HoldStartTime?.Invoke() > HitObject.StartTime) - return; + var startTime = HoldStartTime?.Invoke(); - AddJudgement(new HoldNoteTickJudgement { Result = HitResult.Perfect }); - } - - protected override void Update() - { - if (AllJudged) - return; - - if (HoldStartTime?.Invoke() == null) - return; - - UpdateJudgement(true); + if (startTime == null || startTime > HitObject.StartTime) + ApplyResult(r => r.Type = HitResult.Miss); + else + ApplyResult(r => r.Type = HitResult.Perfect); } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 18084c4c08..7567f40b2f 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -6,7 +6,6 @@ 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.Scoring; using osu.Game.Rulesets.UI.Scrolling; @@ -56,12 +55,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - AddJudgement(new ManiaJudgement { Result = HitResult.Miss }); + ApplyResult(r => r.Type = HitResult.Miss); return; } @@ -69,7 +68,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (result == HitResult.None) return; - AddJudgement(new ManiaJudgement { Result = result }); + ApplyResult(r => r.Type = result); } public virtual bool OnPressed(ManiaAction action) @@ -77,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; - return UpdateJudgement(true); + return UpdateResult(true); } public virtual bool OnReleased(ManiaAction action) => false; diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 22fa93a308..77562bb4c2 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -3,6 +3,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Mania.Objects @@ -55,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// /// The tail note of the hold. /// - public readonly Note Tail = new Note(); + public readonly TailNote Tail = new TailNote(); /// /// The time between ticks of this hold. @@ -68,9 +70,6 @@ namespace osu.Game.Rulesets.Mania.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate; - - Head.ApplyDefaults(controlPointInfo, difficulty); - Tail.ApplyDefaults(controlPointInfo, difficulty); } protected override void CreateNestedHitObjects() @@ -78,6 +77,9 @@ namespace osu.Game.Rulesets.Mania.Objects base.CreateNestedHitObjects(); createTicks(); + + AddNested(Head); + AddNested(Tail); } private void createTicks() @@ -94,5 +96,7 @@ namespace osu.Game.Rulesets.Mania.Objects }); } } + + public override Judgement CreateJudgement() => new HoldNoteJudgement(); } } diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs index d078c15c92..05959a31c0 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNoteTick.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 osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; + namespace osu.Game.Rulesets.Mania.Objects { /// @@ -8,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Objects /// public class HoldNoteTick : ManiaHitObject { + public override Judgement CreateJudgement() => new HoldNoteTickJudgement(); } } diff --git a/osu.Game.Rulesets.Mania/Objects/Note.cs b/osu.Game.Rulesets.Mania/Objects/Note.cs index 975188e550..42877649d2 100644 --- a/osu.Game.Rulesets.Mania/Objects/Note.cs +++ b/osu.Game.Rulesets.Mania/Objects/Note.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 osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; + namespace osu.Game.Rulesets.Mania.Objects { /// @@ -8,5 +11,6 @@ namespace osu.Game.Rulesets.Mania.Objects /// public class Note : ManiaHitObject { + public override Judgement CreateJudgement() => new ManiaJudgement(); } } diff --git a/osu.Game.Rulesets.Mania/Objects/TailNote.cs b/osu.Game.Rulesets.Mania/Objects/TailNote.cs new file mode 100644 index 0000000000..9de542bcd3 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/TailNote.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; + +namespace osu.Game.Rulesets.Mania.Objects +{ + public class TailNote : Note + { + public override Judgement CreateJudgement() => new ManiaJudgement(); + } +} diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 4c955a680e..12b32c46ee 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.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.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; @@ -97,31 +96,20 @@ namespace osu.Game.Rulesets.Mania.Scoring { } - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void ApplyBeatmap(Beatmap beatmap) { + base.ApplyBeatmap(beatmap); + BeatmapDifficulty difficulty = beatmap.BeatmapInfo.BaseDifficulty; hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max); hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max); + } + protected override void SimulateAutoplay(Beatmap beatmap) + { while (true) { - foreach (var obj in beatmap.HitObjects) - { - var holdNote = obj as HoldNote; - - if (holdNote != null) - { - // Head - AddJudgement(new ManiaJudgement { Result = HitResult.Perfect }); - - // Ticks - int tickCount = holdNote.NestedHitObjects.OfType().Count(); - for (int i = 0; i < tickCount; i++) - AddJudgement(new HoldNoteTickJudgement { Result = HitResult.Perfect }); - } - - AddJudgement(new ManiaJudgement { Result = HitResult.Perfect }); - } + base.SimulateAutoplay(beatmap); if (!HasFailed) break; @@ -133,20 +121,20 @@ namespace osu.Game.Rulesets.Mania.Scoring } } - protected override void OnNewJudgement(Judgement judgement) + protected override void ApplyResult(JudgementResult result) { - base.OnNewJudgement(judgement); + base.ApplyResult(result); - bool isTick = judgement is HoldNoteTickJudgement; + bool isTick = result.Judgement is HoldNoteTickJudgement; if (isTick) { - if (judgement.IsHit) + if (result.IsHit) Health.Value += hpMultiplier * hp_increase_tick; } else { - switch (judgement.Result) + switch (result.Type) { case HitResult.Miss: Health.Value += hpMissMultiplier * hp_increase_miss; diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 877189dd61..d489d48fc3 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -131,14 +131,14 @@ namespace osu.Game.Rulesets.Mania.UI public override void Add(DrawableHitObject hitObject) { hitObject.AccentColour = AccentColour; - hitObject.OnJudgement += OnJudgement; + hitObject.OnNewResult += OnNewResult; HitObjects.Add(hitObject); } - internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { - if (!judgement.IsHit || !judgedObject.DisplayJudgement || !DisplayJudgements) + if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements) return; explosionContainer.Add(new HitExplosion(judgedObject) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 6566d44ef5..dc66249cd9 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -8,10 +8,10 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI { - internal class DrawableManiaJudgement : DrawableJudgement + public class DrawableManiaJudgement : DrawableJudgement { - public DrawableManiaJudgement(Judgement judgement, DrawableHitObject judgedObject) - : base(judgement, judgedObject) + public DrawableManiaJudgement(JudgementResult result, DrawableHitObject judgedObject) + : base(result, judgedObject) { } @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.UI this.FadeInFromZero(50, Easing.OutQuint); - if (Judgement.IsHit) + if (Result.IsHit) { this.ScaleTo(0.8f); this.ScaleTo(1, 250, Easing.OutElastic); diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index f88169726e..6bf63443b5 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -1,22 +1,20 @@ // 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.Rulesets.Mania.Objects; using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI { public class ManiaPlayfield : ManiaScrollingPlayfield { - public List Columns => stages.SelectMany(x => x.Columns).ToList(); private readonly List stages = new List(); public ManiaPlayfield(List stageDefinitions) diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index aaa4505b5e..09ebde2799 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Input; @@ -35,8 +34,7 @@ namespace osu.Game.Rulesets.Mania.UI public IEnumerable BarLines; - private readonly Bindable configDirection = new Bindable(); - private ScrollingInfo scrollingInfo; + protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config; public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -73,9 +71,6 @@ namespace osu.Game.Rulesets.Mania.UI private void load() { BarLines.ForEach(Playfield.Add); - - ((ManiaConfigManager)Config).BindWith(ManiaSetting.ScrollDirection, configDirection); - configDirection.BindValueChanged(d => scrollingInfo.Direction.Value = (ScrollingDirection)d, true); } private DependencyContainer dependencies; @@ -83,11 +78,14 @@ namespace osu.Game.Rulesets.Mania.UI protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(scrollingInfo = new ScrollingInfo()); + + if (dependencies.Get() == null) + dependencies.CacheAs(new ManiaScrollingInfo(Config)); + return dependencies; } - protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages) + protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -115,11 +113,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); - - private class ScrollingInfo : IScrollingInfo - { - public readonly Bindable Direction = new Bindable(); - IBindable IScrollingInfo.Direction => Direction; - } } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs new file mode 100644 index 0000000000..624ea13e1b --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.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 osu.Framework.Configuration; +using osu.Game.Rulesets.Mania.Configuration; +using osu.Game.Rulesets.UI.Scrolling; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class ManiaScrollingInfo : IScrollingInfo + { + private readonly Bindable configDirection = new Bindable(); + + public readonly Bindable Direction = new Bindable(); + IBindable IScrollingInfo.Direction => Direction; + + public ManiaScrollingInfo(ManiaConfigManager config) + { + config.BindWith(ManiaSetting.ScrollDirection, configDirection); + configDirection.BindValueChanged(v => Direction.Value = (ScrollingDirection)v, true); + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index f386cf15a2..f292d5ff16 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// A collection of s. /// - internal class ManiaStage : ManiaScrollingPlayfield + public class ManiaStage : ManiaScrollingPlayfield { public const float HIT_TARGET_POSITION = 50; @@ -156,18 +156,18 @@ namespace osu.Game.Rulesets.Mania.UI var maniaObject = (ManiaHitObject)h.HitObject; int columnIndex = maniaObject.Column - firstColumnIndex; Columns.ElementAt(columnIndex).Add(h); - h.OnJudgement += OnJudgement; + h.OnNewResult += OnNewResult; } public void Add(BarLine barline) => base.Add(new DrawableBarLine(barline)); - internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { - if (!judgedObject.DisplayJudgement || !DisplayJudgements) + if (!judgedObject.DisplayResult || !DisplayJudgements) return; judgements.Clear(); - judgements.Add(new DrawableManiaJudgement(judgement, judgedObject) + judgements.Add(new DrawableManiaJudgement(result, judgedObject) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json index 13aba025fd..fe3ecbec47 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/launch.json @@ -2,35 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "VisualTests (Debug, net471)", - "windows": { - "type": "clr" - }, - "type": "mono", - "request": "launch", - "program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Osu.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}/bin/Release/net471/osu.Game.Rulesets.Osu.Tests.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, msbuild)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - }, - { - "name": "VisualTests (Debug, netcoreapp2.1)", + "name": "VisualTests (Debug)", "type": "coreclr", "request": "launch", "program": "dotnet", @@ -38,12 +10,12 @@ "${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Osu.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Debug, dotnet)", + "preLaunchTask": "Build (Debug)", "env": {}, "console": "internalConsole" }, { - "name": "VisualTests (Release, netcoreapp2.1)", + "name": "VisualTests (Release)", "type": "coreclr", "request": "launch", "program": "dotnet", @@ -51,7 +23,7 @@ "${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Osu.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, dotnet)", + "preLaunchTask": "Build (Release)", "env": {}, "console": "internalConsole" } diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json index 62cf51382f..ed2a015e11 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json @@ -4,43 +4,13 @@ "version": "2.0.0", "tasks": [ { - "label": "Build (Debug, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "osu.Game.Rulesets.Osu.Tests.csproj", - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Release, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "osu.Game.Rulesets.Osu.Tests.csproj", - "/p:Configuration=Release", - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Debug, dotnet)", + "label": "Build (Debug)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", - "/p:TargetFramework=netcoreapp2.1", "/p:GenerateFullPaths=true", "/m", "/verbosity:m" @@ -49,14 +19,13 @@ "problemMatcher": "$msCompile" }, { - "label": "Build (Release, dotnet)", + "label": "Build (Release)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", - "/p:TargetFramework=netcoreapp2.1", "/p:Configuration=Release", "/p:GenerateFullPaths=true", "/m", @@ -66,16 +35,7 @@ "problemMatcher": "$msCompile" }, { - "label": "Restore (net471)", - "type": "shell", - "command": "nuget", - "args": [ - "restore" - ], - "problemMatcher": [] - }, - { - "label": "Restore (netcoreapp2.1)", + "label": "Restore", "type": "shell", "command": "dotnet", "args": [ diff --git a/osu.Game.Rulesets.Osu.Tests/StackingTest.cs b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs new file mode 100644 index 0000000000..579cb77084 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/StackingTest.cs @@ -0,0 +1,64 @@ +// 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 System.Text; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class StackingTest + { + [Test] + public void TestStacking() + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(beatmap_data))) + using (var reader = new StreamReader(stream)) + { + var beatmap = Decoder.GetDecoder(reader).Decode(reader); + var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo); + + var objects = converted.HitObjects.ToList(); + + // The last hitobject triggers the stacking + for (int i = 0; i < objects.Count - 1; i++) + Assert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight); + } + } + + private const string beatmap_data = @" +osu file format v14 + +[General] +StackLeniency: 0.2 + +[Difficulty] +ApproachRate:9.2 +SliderMultiplier:1 +SliderTickRate:0.5 + +[TimingPoints] +217871,6400,4,2,1,20,1,0 +217871,-800,4,2,1,20,0,0 +218071,-787.5,4,2,1,20,0,0 +218271,-775,4,2,1,20,0,0 +218471,-762.5,4,2,1,20,0,0 +218671,-750,4,2,1,20,0,0 +240271,-10,4,2,0,5,0,0 + +[HitObjects] +311,185,217871,6,0,L|318:158,1,25 +311,185,218071,2,0,L|335:170,1,25 +311,185,218271,2,0,L|338:192,1,25 +311,185,218471,2,0,L|325:209,1,25 +311,185,218671,2,0,L|304:212,1,25 +311,185,240271,5,0,0:0:0:0: +"; + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs index 7af7140fd8..c2d3aab2ab 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircle.cs @@ -5,12 +5,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; using OpenTK; -using osu.Game.Rulesets.Osu.Judgements; using System.Collections.Generic; using System; using osu.Game.Rulesets.Mods; @@ -96,19 +94,15 @@ namespace osu.Game.Rulesets.Osu.Tests this.auto = auto; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (auto && !userTriggered && timeOffset > 0) { // force success - AddJudgement(new OsuJudgement - { - Result = HitResult.Great - }); - State.Value = ArmedState.Hit; + ApplyResult(r => r.Type = HitResult.Great); } else - base.CheckForJudgements(userTriggered, timeOffset); + base.CheckForResult(userTriggered, timeOffset); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs index cb1ea5cc5f..3f9464a98f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs @@ -304,13 +304,13 @@ namespace osu.Game.Rulesets.Osu.Tests foreach (var mod in Mods.OfType()) mod.ApplyToDrawableHitObjects(new[] { drawable }); - drawable.OnJudgement += onJudgement; + drawable.OnNewResult += onNewResult; Add(drawable); } private float judgementOffsetDirection = 1; - private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { var osuObject = judgedObject as DrawableOsuHitObject; if (osuObject == null) @@ -321,8 +321,8 @@ namespace osu.Game.Rulesets.Osu.Tests { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = judgement.IsHit ? "Hit!" : "Miss!", - Colour = judgement.IsHit ? Color4.Green : Color4.Red, + Text = result.IsHit ? "Hit!" : "Miss!", + Colour = result.IsHit ? Color4.Green : Color4.Red, TextSize = 30, Position = osuObject.HitObject.StackedEndPosition + judgementOffsetDirection * new Vector2(0, 45) }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSpinner.cs index b05a763e88..3b91ea93b8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestCaseSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSpinner.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Tests this.auto = auto; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1) { @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Tests auto = false; } - base.CheckForJudgements(userTriggered, timeOffset); + base.CheckForResult(userTriggered, timeOffset); } } } 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 c5d9b26145..247d5e18c1 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 @@ -1,8 +1,14 @@  + + + + + + WinExe - netcoreapp2.1;net471 + netcoreapp2.1 diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 405493cde4..9e0e649eb2 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.UI; namespace osu.Game.Rulesets.Osu.Beatmaps { - internal class OsuBeatmapConverter : BeatmapConverter + public class OsuBeatmapConverter : BeatmapConverter { public OsuBeatmapConverter(IBeatmap beatmap) : base(beatmap) @@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps RepeatCount = curveData.RepeatCount, Position = positionData?.Position ?? Vector2.Zero, NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset }; } @@ -52,7 +53,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps StartTime = original.StartTime, Samples = original.Samples, EndTime = endTimeData.EndTime, - Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2 + Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2, + NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, }; } else @@ -62,7 +65,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps StartTime = original.StartTime, Samples = original.Samples, Position = positionData?.Position ?? Vector2.Zero, - NewCombo = comboData?.NewCombo ?? false + NewCombo = comboData?.NewCombo ?? false, + ComboOffset = comboData?.ComboOffset ?? 0, }; } } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index bbe2d67baa..cfb1b0f050 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -8,16 +8,16 @@ using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Beatmaps { - internal class OsuBeatmapProcessor : BeatmapProcessor + public class OsuBeatmapProcessor : BeatmapProcessor { public OsuBeatmapProcessor(IBeatmap beatmap) : base(beatmap) { } - public override void PreProcess() + public override void PostProcess() { - base.PreProcess(); + base.PostProcess(); applyStacking((Beatmap)Beatmap); } diff --git a/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs b/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs new file mode 100644 index 0000000000..3000031c78 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/ComboResult.cs @@ -0,0 +1,17 @@ +// 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.Rulesets.Osu.Judgements +{ + public enum ComboResult + { + [Description(@"")] + None, + [Description(@"Good")] + Good, + [Description(@"Amazing")] + Perfect + } +} diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index 26becfdec9..b1c9760866 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Judgements @@ -25,7 +24,5 @@ namespace osu.Game.Rulesets.Osu.Judgements return 300; } } - - public ComboResult Combo; } } diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs new file mode 100644 index 0000000000..17b8b4399f --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgementResult.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Judgements; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuJudgementResult : JudgementResult + { + public ComboResult ComboType; + + public OsuJudgementResult(Judgement judgement) + : base(judgement) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index b2ddd65e38..37d5f57fcb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => "Autopilot"; public override string ShortenedName => "AP"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_autopilot; + public override ModType Type => ModType.Automation; public override string Description => @"Automatic cursor movement - just follow the rhythm."; 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/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 9f9c2a09b6..4047e057cb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -2,14 +2,89 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Linq; +using osu.Framework.Input.States; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; +using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModRelax : ModRelax + public class OsuModRelax : ModRelax, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToRulesetContainer { public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); + + public bool AllowFail => false; + + public void Update(Playfield playfield) + { + bool requiresHold = false; + bool requiresHit = false; + + const float relax_leniency = 3; + + foreach (var drawable in playfield.HitObjects.AliveObjects) + { + if (!(drawable is DrawableOsuHitObject osuHit)) + continue; + + double time = osuHit.Clock.CurrentTime; + double relativetime = time - osuHit.HitObject.StartTime; + + if (time < osuHit.HitObject.StartTime - relax_leniency) continue; + + if (osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime || osuHit.IsHit) + continue; + + requiresHit |= osuHit is DrawableHitCircle && osuHit.IsHovered && osuHit.HitObject.HitWindows.CanBeHit(relativetime); + requiresHold |= osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered) || osuHit is DrawableSpinner; + } + + if (requiresHit) + { + addAction(false); + addAction(true); + } + + addAction(requiresHold); + } + + private bool wasHit; + private bool wasLeft; + + private OsuInputManager osuInputManager; + + private void addAction(bool hitting) + { + if (wasHit == hitting) + return; + + wasHit = hitting; + + var state = new ReplayState + { + PressedActions = new List() + }; + + if (hitting) + { + state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton); + wasLeft = !wasLeft; + } + + osuInputManager.HandleCustomInput(new InputState(), state); + } + + public void ApplyToRulesetContainer(RulesetContainer rulesetContainer) + { + // grab the input manager for future use. + osuInputManager = (OsuInputManager)rulesetContainer.KeyBindingInputManager; + osuInputManager.AllowUserPresses = false; + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index d14af57bab..6aa864d9b2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => "Spun Out"; public override string ShortenedName => "SO"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_spunout; + public override ModType Type => ModType.DifficultyReduction; public override string Description => @"Spinners will be automatically completed."; public override double ScoreMultiplier => 0.9; public override bool Ranked => true; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index ce53857a09..139ce4cc4b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => "Target"; public override string ShortenedName => "TP"; + public override ModType Type => ModType.Conversion; public override FontAwesome Icon => FontAwesome.fa_osu_mod_target; public override string Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index 2c89ddc9cf..908b9cb3c6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { @@ -20,27 +21,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { Origin = Anchor.Centre; - Masking = true; - AutoSizeAxes = Axes.Both; - CornerRadius = width / 2; - EdgeEffect = new EdgeEffectParameters + Child = new SkinnableDrawable("Play/osu/followpoint", _ => new Container { - Type = EdgeEffectType.Glow, - Colour = Color4.White.Opacity(0.2f), - Radius = 4, - }; - - Children = new Drawable[] - { - new Box + Masking = true, + AutoSizeAxes = Axes.Both, + CornerRadius = width / 2, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Color4.White.Opacity(0.2f), + Radius = 4, + }, + Child = new Box { Size = new Vector2(width), Blending = BlendingMode.Additive, Origin = Anchor.Centre, Anchor = Anchor.Centre, Alpha = 0.5f, - }, - }; + } + }, restrictSize: false); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 4ac3b0c983..61a6e6404a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections Vector2 distanceVector = endPosition - startPosition; int distance = (int)distanceVector.Length; - float rotation = (float)Math.Atan2(distanceVector.Y, distanceVector.X); + float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI)); double duration = endTime - startTime; for (int d = (int)(PointDistance * 1.5); d < distance - PointDistance; d += PointDistance) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index c525b4bd97..6344fbb770 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using OpenTK; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; using OpenTK.Graphics; @@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (AllJudged) return false; - UpdateJudgement(true); + UpdateResult(true); return true; }, }, @@ -77,12 +76,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - AddJudgement(new OsuJudgement { Result = HitResult.Miss }); + ApplyResult(r => r.Type = HitResult.Miss); + return; } @@ -90,10 +90,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (result == HitResult.None) return; - AddJudgement(new OsuJudgement - { - Result = result, - }); + ApplyResult(r => r.Type = result); } protected override void UpdatePreemptState() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 02def2189f..0501f8b7a0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -1,11 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.ComponentModel; +using System; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; -using System.Linq; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using OpenTK.Graphics; @@ -32,7 +34,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { UpdatePreemptState(); - using (BeginDelayedSequence(HitObject.TimePreempt + (Judgements.FirstOrDefault()?.TimeOffset ?? 0), true)) + var judgementOffset = Math.Min(HitObject.HitWindows.HalfWindowFor(HitResult.Miss), Result?.TimeOffset ?? 0); + + using (BeginDelayedSequence(HitObject.TimePreempt + judgementOffset, true)) UpdateCurrentState(state); } } @@ -53,20 +57,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Todo: At some point we need to move these to DrawableHitObject after ensuring that all other Rulesets apply // transforms in the same way and don't rely on them not being cleared - public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) { } - public override void ApplyTransformsAt(double time, bool propagateChildren = false) { } + public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) + { + } + + public override void ApplyTransformsAt(double time, bool propagateChildren = false) + { + } private OsuInputManager osuActionInputManager; internal OsuInputManager OsuActionInputManager => osuActionInputManager ?? (osuActionInputManager = GetContainingInputManager() as OsuInputManager); - } - public enum ComboResult - { - [Description(@"")] - None, - [Description(@"Good")] - Good, - [Description(@"Amazing")] - Perfect + protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(judgement); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index e8743281da..04ec3f13c7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -11,14 +11,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuJudgement : DrawableJudgement { - public DrawableOsuJudgement(Judgement judgement, DrawableHitObject judgedObject) - : base(judgement, judgedObject) + public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject) + : base(result, judgedObject) { } protected override void LoadComplete() { - if (Judgement.Result != HitResult.Miss) + if (Result.Type != HitResult.Miss) JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); base.LoadComplete(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index 26f3ee6bb4..dfe7937e81 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -8,8 +8,8 @@ using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Drawables; using OpenTK; using osu.Game.Graphics; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -33,18 +33,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { - new SpriteIcon + new SkinnableDrawable("Play/osu/reversearrow", _ => new SpriteIcon { RelativeSizeAxes = Axes.Both, Icon = FontAwesome.fa_chevron_right - } + }, restrictSize: false) }; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (repeatPoint.StartTime <= Time.Current) - AddJudgement(new OsuJudgement { Result = drawableSlider.Tracking ? HitResult.Great : HitResult.Miss }); + ApplyResult(r => r.Type = drawableSlider.Tracking ? HitResult.Great : HitResult.Miss); } protected override void UpdatePreemptState() @@ -74,6 +74,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + private bool hasRotation; + public void UpdateSnakingPosition(Vector2 start, Vector2 end) { bool isRepeatAtEnd = repeatPoint.RepeatIndex % 2 == 0; @@ -87,15 +89,33 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables int searchStart = isRepeatAtEnd ? curve.Count - 1 : 0; int direction = isRepeatAtEnd ? -1 : 1; + Vector2 aimRotationVector = Vector2.Zero; + // 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 (Precision.AlmostEquals(curve[i], Position)) continue; - Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(curve[i].Y - Position.Y, curve[i].X - Position.X)); + aimRotationVector = curve[i]; break; } + + + float aimRotation = MathHelper.RadiansToDegrees((float)Math.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X)); + while (Math.Abs(aimRotation - Rotation) > 180) + aimRotation += aimRotation < Rotation ? 360 : -360; + + if (!hasRotation) + { + Rotation = aimRotation; + hasRotation = true; + } + else + { + // If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly). + Rotation = Interpolation.ValueAt(MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint); + } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index f1907a92a8..f48f03f197 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; using OpenTK.Graphics; @@ -132,23 +131,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!userTriggered && Time.Current >= slider.EndTime) + if (userTriggered || Time.Current < slider.EndTime) + return; + + ApplyResult(r => { var judgementsCount = NestedHitObjects.Count(); var judgementsHit = NestedHitObjects.Count(h => h.IsHit); var hitFraction = (double)judgementsHit / judgementsCount; - if (hitFraction == 1 && HeadCircle.Judgements.Any(j => j.Result == HitResult.Great)) - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - else if (hitFraction >= 0.5 && HeadCircle.Judgements.Any(j => j.Result >= HitResult.Good)) - AddJudgement(new OsuJudgement { Result = HitResult.Good }); + + if (hitFraction == 1 && HeadCircle.Result.Type == HitResult.Great) + r.Type = HitResult.Great; + else if (hitFraction >= 0.5 && HeadCircle.Result.Type >= HitResult.Good) + r.Type = HitResult.Good; else if (hitFraction > 0) - AddJudgement(new OsuJudgement { Result = HitResult.Meh }); + r.Type = HitResult.Meh; else - AddJudgement(new OsuJudgement { Result = HitResult.Miss }); - } + r.Type = HitResult.Miss; + }); } protected override void UpdateCurrentState(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index fee663963e..45c925b87a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -12,11 +11,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// The judgement text is provided by the . /// - public override bool DisplayJudgement => false; + public override bool DisplayResult => false; public bool Tracking { get; set; } - public DrawableSliderTail(Slider slider, HitCircle hitCircle) + public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle) : base(hitCircle) { Origin = Anchor.Centre; @@ -29,10 +28,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Position = HitObject.Position - slider.Position; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered && timeOffset >= 0) - AddJudgement(new OsuSliderTailJudgement { Result = Tracking ? HitResult.Great : HitResult.Miss }); + ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index db75321eb0..964c75131a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -6,8 +6,9 @@ using osu.Game.Rulesets.Objects.Drawables; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osu.Framework.Graphics.Containers; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -17,35 +18,39 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } - public override bool DisplayJudgement => false; + public override bool DisplayResult => false; public DrawableSliderTick(SliderTick sliderTick) : base(sliderTick) { Size = new Vector2(16) * sliderTick.Scale; - - Masking = true; - CornerRadius = Size.X / 2; - Origin = Anchor.Centre; - BorderThickness = 2; - BorderColour = Color4.White; - InternalChildren = new Drawable[] { - new Box + new SkinnableDrawable("Play/osu/sliderscorepoint", _ => new Container { + Masking = true, RelativeSizeAxes = Axes.Both, - Colour = AccentColour, - Alpha = 0.3f, - } + Origin = Anchor.Centre, + CornerRadius = Size.X / 2, + + BorderThickness = 2, + BorderColour = Color4.White, + + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = AccentColour, + Alpha = 0.3f, + } + }, restrictSize: false) }; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset >= 0) - AddJudgement(new OsuJudgement { Result = Tracking ? HitResult.Great : HitResult.Miss }); + ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss); } protected override void UpdatePreemptState() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 1d3df69fb8..51b1990a21 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -11,7 +11,6 @@ using OpenTK.Graphics; using osu.Game.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; -using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Screens.Ranking; using osu.Game.Rulesets.Scoring; @@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public float Progress => MathHelper.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1); - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Time.Current < HitObject.StartTime) return; @@ -136,17 +135,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables glow.FadeColour(completeColour, duration); } - if (!userTriggered && Time.Current >= Spinner.EndTime) + if (userTriggered || Time.Current < Spinner.EndTime) + return; + + ApplyResult(r => { if (Progress >= 1) - AddJudgement(new OsuJudgement { Result = HitResult.Great }); + r.Type = HitResult.Great; else if (Progress > .9) - AddJudgement(new OsuJudgement { Result = HitResult.Good }); + r.Type = HitResult.Good; else if (Progress > .75) - AddJudgement(new OsuJudgement { Result = HitResult.Meh }); + r.Type = HitResult.Meh; else if (Time.Current >= Spinner.EndTime) - AddJudgement(new OsuJudgement { Result = HitResult.Miss }); - } + r.Type = HitResult.Miss; + }); } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 92c42af861..b11e4fc971 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -8,8 +8,8 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.EventArgs; using osu.Framework.Input.States; using osu.Game.Rulesets.Objects.Types; -using OpenTK; using OpenTK.Graphics; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private const float width = 128; private Color4 accentColour = Color4.Black; + /// /// The colour that is used for the slider ball. /// @@ -27,14 +28,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces set { accentColour = value; - if (ball != null) - ball.Colour = value; + if (drawableBall != null) + drawableBall.Colour = value; } } private readonly Slider slider; - public readonly Box FollowCircle; - private readonly Box ball; + public readonly Drawable FollowCircle; + private Drawable drawableBall; public SliderBall(Slider slider) { @@ -43,19 +44,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces AutoSizeAxes = Axes.Both; Blending = BlendingMode.Additive; Origin = Anchor.Centre; - BorderThickness = 10; - BorderColour = Color4.Orange; - Children = new Drawable[] + Children = new[] { - FollowCircle = new Box + FollowCircle = new Container { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Colour = Color4.Orange, Width = width, Height = width, Alpha = 0, + Child = new SkinnableDrawable("Play/osu/sliderfollowcircle", _ => new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = 5, + BorderColour = Color4.Orange, + Blending = BlendingMode.Additive, + Child = new Box + { + Colour = Color4.Orange, + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f, + } + }), }, new CircularContainer { @@ -63,18 +75,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces AutoSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, - BorderThickness = 10, - BorderColour = Color4.White, Alpha = 1, - Children = new[] + Child = new Container { - ball = new Box + Width = width, + Height = width, + // TODO: support skin filename animation (sliderb0, sliderb1...) + Child = new SkinnableDrawable("Play/osu/sliderb", _ => new CircularContainer { - Colour = AccentColour, - Alpha = 0.4f, - Width = width, - Height = width, - }, + Masking = true, + RelativeSizeAxes = Axes.Both, + BorderThickness = 10, + BorderColour = Color4.White, + Alpha = 1, + Child = drawableBall = new Box + { + Colour = AccentColour, + RelativeSizeAxes = Axes.Both, + Alpha = 0.4f, + } + }), } } }; @@ -100,9 +120,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return base.OnMouseMove(state); } - // If the current time is between the start and end of the slider, we should track mouse input regardless of the cursor position. - public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => canCurrentlyTrack || base.ReceiveMouseInputAt(screenSpacePos); - public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) { // Consider the case of rewinding - children's transforms are handled internally, so propagating down @@ -111,6 +128,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } private bool tracking; + public bool Tracking { get { return tracking; } @@ -120,8 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; tracking = value; - FollowCircle.ScaleTo(tracking ? 2.8f : 1, 300, Easing.OutQuint); - FollowCircle.FadeTo(tracking ? 0.2f : 0, 300, Easing.OutQuint); + FollowCircle.ScaleTo(tracking ? 2f : 1, 300, Easing.OutQuint); + FollowCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint); } } @@ -136,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces // Make sure to use the base version of ReceiveMouseInputAt so that we correctly check the position. Tracking = canCurrentlyTrack && lastState != null - && base.ReceiveMouseInputAt(lastState.Mouse.NativeState.Position) + && ReceiveMouseInputAt(lastState.Mouse.NativeState.Position) && ((Parent as DrawableSlider)?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); } } diff --git a/osu.Game.Rulesets.Osu/Objects/HitCircle.cs b/osu.Game.Rulesets.Osu/Objects/HitCircle.cs index 9e309a376d..d1656a9672 100644 --- a/osu.Game.Rulesets.Osu/Objects/HitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/HitCircle.cs @@ -1,9 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; + namespace osu.Game.Rulesets.Osu.Objects { public class HitCircle : OsuHitObject { + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 48a6365c00..fdf5aaffa8 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -54,6 +54,8 @@ namespace osu.Game.Rulesets.Osu.Objects public virtual bool NewCombo { get; set; } + public int ComboOffset { get; set; } + public virtual int IndexInCurrentCombo { get; set; } public virtual int ComboIndex { get; set; } diff --git a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs index 0b729f0956..c8621cdbcf 100644 --- a/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/RepeatPoint.cs @@ -4,6 +4,8 @@ using System; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.Objects { @@ -16,10 +18,15 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + // Out preempt should be one span early to give the user ample warning. + TimePreempt += SpanDuration; + // We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders // we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time. if (RepeatIndex > 0) TimePreempt = Math.Min(SpanDuration * 2, TimePreempt); } + + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 698f9de787..7a0dcc77a6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -10,6 +10,8 @@ using System.Linq; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.Objects { @@ -94,7 +96,7 @@ namespace osu.Game.Rulesets.Osu.Objects public double TickDistance; public HitCircle HeadCircle; - public HitCircle TailCircle; + public SliderTailCircle TailCircle; protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { @@ -133,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects ComboIndex = ComboIndex, }; - TailCircle = new SliderCircle(this) + TailCircle = new SliderTailCircle(this) { StartTime = EndTime, Position = EndPosition, @@ -211,5 +213,7 @@ namespace osu.Game.Rulesets.Osu.Objects }); } } + + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs new file mode 100644 index 0000000000..23616ea005 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.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 osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SliderTailCircle : SliderCircle + { + public SliderTailCircle(Slider slider) + : base(slider) + { + } + + public override Judgement CreateJudgement() => new OsuSliderTailJudgement(); + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 54337a12be..906f0a0182 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -3,6 +3,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.Objects { @@ -26,5 +28,7 @@ namespace osu.Game.Rulesets.Osu.Objects TimePreempt = (StartTime - SpanStartTime) / 2 + offset; } + + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 503ad85674..1c60fd4831 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -5,6 +5,8 @@ using System; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; namespace osu.Game.Rulesets.Osu.Objects { @@ -18,8 +20,6 @@ namespace osu.Game.Rulesets.Osu.Objects /// public int SpinsRequired { get; protected set; } = 1; - public override bool NewCombo => true; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -29,5 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); } + + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index d9ae836e0a..e7bbe755a0 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.ComponentModel; using osu.Framework.Input.Bindings; +using osu.Framework.Input.EventArgs; +using osu.Framework.Input.States; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu @@ -12,8 +14,35 @@ namespace osu.Game.Rulesets.Osu { public IEnumerable PressedActions => KeyBindingContainer.PressedActions; - public OsuInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique) + public bool AllowUserPresses { + set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value; + } + + protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + => new OsuKeyBindingContainer(ruleset, variant, unique); + + public OsuInputManager(RulesetInfo ruleset) + : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + + private class OsuKeyBindingContainer : RulesetKeyBindingContainer + { + public bool AllowUserPresses = true; + + public OsuKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => AllowUserPresses && base.OnKeyDown(state, args); + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => AllowUserPresses && base.OnKeyUp(state, args); + protected override bool OnJoystickPress(InputState state, JoystickEventArgs args) => AllowUserPresses && base.OnJoystickPress(state, args); + protected override bool OnJoystickRelease(InputState state, JoystickEventArgs args) => AllowUserPresses && base.OnJoystickRelease(state, args); + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => AllowUserPresses && base.OnMouseDown(state, args); + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => AllowUserPresses && base.OnMouseUp(state, args); + protected override bool OnScroll(InputState state) => AllowUserPresses && base.OnScroll(state); } } @@ -21,6 +50,7 @@ namespace osu.Game.Rulesets.Osu { [Description("Left Button")] LeftButton, + [Description("Right Button")] RightButton } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index b8ba1e2945..fa6e9a018a 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -94,6 +94,7 @@ namespace osu.Game.Rulesets.Osu new OsuModEasy(), new OsuModNoFail(), new MultiMod(new OsuModHalfTime(), new OsuModDaycore()), + new OsuModSpunOut(), }; case ModType.DifficultyIncrease: return new Mod[] @@ -104,14 +105,17 @@ namespace osu.Game.Rulesets.Osu new OsuModHidden(), new OsuModFlashlight(), }; - case ModType.Special: + case ModType.Conversion: return new Mod[] { + new OsuModTarget(), + }; + case ModType.Automation: + return new Mod[] + { + new MultiMod(new OsuModAutoplay(), new ModCinema()), new OsuModRelax(), new OsuModAutopilot(), - new OsuModSpunOut(), - new MultiMod(new OsuModAutoplay(), new ModCinema()), - new OsuModTarget(), }; default: return new Mod[] { }; diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 01b92255ae..a9d39e88b4 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -2,13 +2,11 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using System.Linq; using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -26,28 +24,11 @@ namespace osu.Game.Rulesets.Osu.Scoring private readonly Dictionary scoreResultCounts = new Dictionary(); private readonly Dictionary comboResultCounts = new Dictionary(); - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void ApplyBeatmap(Beatmap beatmap) { + base.ApplyBeatmap(beatmap); + hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; - - foreach (var obj in beatmap.HitObjects) - { - if (obj is Slider slider) - { - // Head - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - - // Ticks - foreach (var unused in slider.NestedHitObjects.OfType()) - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - - //Repeats - foreach (var unused in slider.NestedHitObjects.OfType()) - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - } - - AddJudgement(new OsuJudgement { Result = HitResult.Great }); - } } protected override void Reset(bool storeResults) @@ -70,19 +51,19 @@ namespace osu.Game.Rulesets.Osu.Scoring private const double harshness = 0.01; - protected override void OnNewJudgement(Judgement judgement) + protected override void ApplyResult(JudgementResult result) { - base.OnNewJudgement(judgement); + base.ApplyResult(result); - var osuJudgement = (OsuJudgement)judgement; + var osuResult = (OsuJudgementResult)result; - if (judgement.Result != HitResult.None) + if (result.Type != HitResult.None) { - scoreResultCounts[judgement.Result] = scoreResultCounts.GetOrDefault(judgement.Result) + 1; - comboResultCounts[osuJudgement.Combo] = comboResultCounts.GetOrDefault(osuJudgement.Combo) + 1; + scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; + comboResultCounts[osuResult.ComboType] = comboResultCounts.GetOrDefault(osuResult.ComboType) + 1; } - switch (judgement.Result) + switch (result.Type) { case HitResult.Great: Health.Value += (10.2 - hpDrainRate) * harshness; @@ -105,5 +86,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break; } } + + protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(judgement); } } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 0532fe0223..abcd1ddbda 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor if (Shared.VertexBuffer == null) Shared.VertexBuffer = new QuadVertexBuffer(max_sprites, BufferUsageHint.DynamicDraw); - Shader.GetUniform("g_FadeClock").Value = Time; + Shader.GetUniform("g_FadeClock").UpdateValue(ref Time); int updateStart = -1, updateEnd = 0; for (int i = 0; i < Parts.Length; ++i) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index b0ba9afee6..703d8764fc 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.UI public override void Add(DrawableHitObject h) { - h.OnJudgement += onJudgement; + h.OnNewResult += onNewResult; var c = h as IDrawableHitObjectWithProxiedApproach; if (c != null) @@ -64,12 +64,12 @@ namespace osu.Game.Rulesets.Osu.UI connectionLayer.HitObjects = HitObjects.Objects.Select(d => d.HitObject).OfType(); } - private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { - if (!judgedObject.DisplayJudgement || !DisplayJudgements) + if (!judgedObject.DisplayResult || !DisplayJudgements) return; - DrawableOsuJudgement explosion = new DrawableOsuJudgement(judgement, judgedObject) + DrawableOsuJudgement explosion = new DrawableOsuJudgement(result, judgedObject) { Origin = Anchor.Centre, Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json index df49e177dc..de7bf77070 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/launch.json @@ -2,35 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "VisualTests (Debug, net471)", - "windows": { - "type": "clr" - }, - "type": "mono", - "request": "launch", - "program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Taiko.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}/bin/Release/net471/osu.Game.Rulesets.Taiko.Tests.exe", - "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, msbuild)", - "runtimeExecutable": null, - "env": {}, - "console": "internalConsole" - }, - { - "name": "VisualTests (Debug, netcoreapp2.1)", + "name": "VisualTests (Debug)", "type": "coreclr", "request": "launch", "program": "dotnet", @@ -38,12 +10,12 @@ "${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Taiko.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Debug, dotnet)", + "preLaunchTask": "Build (Debug)", "env": {}, "console": "internalConsole" }, { - "name": "VisualTests (Release, netcoreapp2.1)", + "name": "VisualTests (Release)", "type": "coreclr", "request": "launch", "program": "dotnet", @@ -51,7 +23,7 @@ "${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Taiko.Tests.dll" ], "cwd": "${workspaceRoot}", - "preLaunchTask": "Build (Release, dotnet)", + "preLaunchTask": "Build (Release)", "env": {}, "console": "internalConsole" } diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json index 7c8beed00f..9b91f2c9b9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json @@ -4,43 +4,13 @@ "version": "2.0.0", "tasks": [ { - "label": "Build (Debug, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "osu.Game.Rulesets.Taiko.Tests.csproj", - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Release, msbuild)", - "type": "shell", - "command": "msbuild", - "args": [ - "osu.Game.Rulesets.Taiko.Tests.csproj", - "/p:Configuration=Release", - "/p:TargetFramework=net471", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" - ], - "group": "build", - "problemMatcher": "$msCompile" - }, - { - "label": "Build (Debug, dotnet)", + "label": "Build (Debug)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", - "/p:TargetFramework=netcoreapp2.1", "/p:GenerateFullPaths=true", "/m", "/verbosity:m" @@ -49,14 +19,13 @@ "problemMatcher": "$msCompile" }, { - "label": "Build (Release, dotnet)", + "label": "Build (Release)", "type": "shell", "command": "dotnet", "args": [ "build", "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", - "/p:TargetFramework=netcoreapp2.1", "/p:Configuration=Release", "/p:GenerateFullPaths=true", "/m", @@ -66,16 +35,7 @@ "problemMatcher": "$msCompile" }, { - "label": "Restore (net471)", - "type": "shell", - "command": "nuget", - "args": [ - "restore" - ], - "problemMatcher": [] - }, - { - "label": "Restore (netcoreapp2.1)", + "label": "Restore", "type": "shell", "command": "dotnet", "args": [ diff --git a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs index 1bf24a46bc..fc103e4c72 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs @@ -8,9 +8,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.MathUtils; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; @@ -39,8 +39,10 @@ namespace osu.Game.Rulesets.Taiko.Tests [BackgroundDependencyLoader] private void load() { - AddStep("Hit!", () => addHitJudgement(false)); + AddStep("Hit", () => addHitJudgement(false)); + AddStep("Strong hit", () => addStrongHitJudgement(false)); AddStep("Kiai hit", () => addHitJudgement(true)); + AddStep("Strong kiai hit", () => addStrongHitJudgement(true)); AddStep("Miss :(", addMissJudgement); AddStep("DrumRoll", () => addDrumRoll(false)); AddStep("Strong DrumRoll", () => addDrumRoll(true)); @@ -78,15 +80,12 @@ namespace osu.Game.Rulesets.Taiko.Tests ControlPointInfo = controlPointInfo }); - var rateAdjustClock = new StopwatchClock(true) { Rate = 1 }; - Add(playfieldContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Height = 768, - Clock = new FramedClock(rateAdjustClock), Children = new[] { rulesetContainer = new TaikoRulesetContainer(new TaikoRuleset(), beatmap) } }); } @@ -133,28 +132,35 @@ namespace osu.Game.Rulesets.Taiko.Tests HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; var cpi = new ControlPointInfo(); - cpi.EffectPoints.Add(new EffectControlPoint - { - KiaiMode = kiai - }); + cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai }); Hit hit = new Hit(); hit.ApplyDefaults(cpi, new BeatmapDifficulty()); var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; - ((TaikoPlayfield)rulesetContainer.Playfield).OnJudgement(h, new TaikoJudgement { Result = hitResult }); + ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(h, new JudgementResult(new TaikoJudgement()) { Type = hitResult }); + } - if (RNG.Next(10) == 0) - { - ((TaikoPlayfield)rulesetContainer.Playfield).OnJudgement(h, new TaikoJudgement { Result = hitResult }); - ((TaikoPlayfield)rulesetContainer.Playfield).OnJudgement(h, new TaikoStrongHitJudgement()); - } + private void addStrongHitJudgement(bool kiai) + { + HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; + + var cpi = new ControlPointInfo(); + cpi.EffectPoints.Add(new EffectControlPoint { KiaiMode = kiai }); + + Hit hit = new Hit(); + hit.ApplyDefaults(cpi, new BeatmapDifficulty()); + + var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; + + ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(h, new JudgementResult(new TaikoJudgement()) { Type = hitResult }); + ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new TaikoStrongJudgement()) { Type = HitResult.Great }); } private void addMissJudgement() { - ((TaikoPlayfield)rulesetContainer.Playfield).OnJudgement(new DrawableTestHit(new Hit()), new TaikoJudgement { Result = HitResult.Miss }); + ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(new DrawableTestHit(new Hit()), new JudgementResult(new TaikoJudgement()) { Type = HitResult.Miss }); } private void addBarLine(bool major, double delay = scroll_time) @@ -204,10 +210,7 @@ namespace osu.Game.Rulesets.Taiko.Tests h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - if (strong) - rulesetContainer.Playfield.Add(new DrawableCentreHitStrong(h)); - else - rulesetContainer.Playfield.Add(new DrawableCentreHit(h)); + rulesetContainer.Playfield.Add(new DrawableCentreHit(h)); } private void addRimHit(bool strong) @@ -220,10 +223,17 @@ namespace osu.Game.Rulesets.Taiko.Tests h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - if (strong) - rulesetContainer.Playfield.Add(new DrawableRimHitStrong(h)); - else - rulesetContainer.Playfield.Add(new DrawableRimHit(h)); + rulesetContainer.Playfield.Add(new DrawableRimHit(h)); + } + + private class TestStrongNestedHit : DrawableStrongNestedHit + { + public TestStrongNestedHit(DrawableHitObject mainObject) + : base(null, mainObject) + { + } + + public override bool OnPressed(TaikoAction action) => false; } private class DrawableTestHit : DrawableHitObject 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 dea34d25e7..08a0579561 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 @@ -1,8 +1,14 @@  + + + + + + WinExe - netcoreapp2.1;net471 + netcoreapp2.1 diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 41972b5d20..acd6d43284 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -168,7 +168,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { StartTime = obj.StartTime, Samples = obj.Samples, - IsStrong = strong, Duration = endTimeData.Duration, RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier) }; diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs index 608f1f9be2..81a1bd1344 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoIntermediateSwellJudgement.cs @@ -7,15 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoIntermediateSwellJudgement : TaikoJudgement { - public override HitResult MaxResult => HitResult.Perfect; + public override HitResult MaxResult => HitResult.Great; public override bool AffectsCombo => false; - public TaikoIntermediateSwellJudgement() - { - Final = false; - } - /// /// Computes the numeric result value for the combo portion of the score. /// diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongHitJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs similarity index 64% rename from osu.Game.Rulesets.Taiko/Judgements/TaikoStrongHitJudgement.cs rename to osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs index 288ad236aa..ccfdeb5b0e 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongHitJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs @@ -3,13 +3,8 @@ namespace osu.Game.Rulesets.Taiko.Judgements { - public class TaikoStrongHitJudgement : TaikoJudgement + public class TaikoStrongJudgement : TaikoJudgement { public override bool AffectsCombo => false; - - public TaikoStrongHitJudgement() - { - Final = true; - } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index dda96c2caf..a6e9972dd3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableCentreHit : DrawableHit { - protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; + public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; public DrawableCentreHit(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs deleted file mode 100644 index a2dabf2b18..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs +++ /dev/null @@ -1,26 +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.Allocation; -using osu.Game.Graphics; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public class DrawableCentreHitStrong : DrawableHitStrong - { - protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; - - public DrawableCentreHitStrong(Hit hit) - : base(hit) - { - MainPiece.Add(new CentreHitSymbolPiece()); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - MainPiece.AccentColour = colours.PinkDarker; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 00eac4adca..5142f125ac 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.MathUtils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Judgements; using OpenTK; using OpenTK.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; @@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables foreach (var tick in drumRoll.NestedHitObjects.OfType()) { var newTick = new DrawableDrumRollTick(tick); - newTick.OnJudgement += onTickJudgement; + newTick.OnNewResult += onNewTickResult; AddNested(newTick); tickContainer.Add(newTick); @@ -61,9 +60,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables colourEngaged = colours.YellowDarker; } - private void onTickJudgement(DrawableHitObject obj, Judgement judgement) + private void onNewTickResult(DrawableHitObject obj, JudgementResult result) { - if (judgement.Result > HitResult.Miss) + if (result.Type > HitResult.Miss) rollingHits++; else rollingHits--; @@ -74,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece.FadeAccent(newColour, 100); } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (userTriggered) return; @@ -84,13 +83,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables int countHit = NestedHitObjects.Count(o => o.IsHit); if (countHit >= HitObject.RequiredGoodHits) - { - AddJudgement(new TaikoJudgement { Result = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good }); - if (HitObject.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); - } + ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); else - AddJudgement(new TaikoJudgement { Result = HitResult.Miss }); + ApplyResult(r => r.Type = HitResult.Miss); } protected override void UpdateState(ArmedState state) @@ -103,5 +98,25 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables break; } } + + protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + + private class StrongNestedHit : DrawableStrongNestedHit + { + public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll) + : base(strong, drumRoll) + { + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (!MainObject.Judged) + return; + + ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss); + } + + public override bool OnPressed(TaikoAction action) => false; + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 7a57cf77b4..a70d7bde0e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -18,24 +17,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables FillMode = FillMode.Fit; } - public override bool DisplayJudgement => false; + public override bool DisplayResult => false; protected override TaikoPiece CreateMainPiece() => new TickPiece { Filled = HitObject.FirstTick }; - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) + { + if (timeOffset > HitObject.HitWindow) + ApplyResult(r => r.Type = HitResult.Miss); + return; + } + + if (Math.Abs(timeOffset) > HitObject.HitWindow) return; - if (!(Math.Abs(timeOffset) < HitObject.HitWindow)) - return; - - AddJudgement(new TaikoDrumRollTickJudgement { Result = HitResult.Great }); - if (HitObject.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); + ApplyResult(r => r.Type = HitResult.Great); } protected override void UpdateState(ArmedState state) @@ -48,6 +49,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - public override bool OnPressed(TaikoAction action) => UpdateJudgement(true); + public override bool OnPressed(TaikoAction action) => UpdateResult(true); + + protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + + private class StrongNestedHit : DrawableStrongNestedHit + { + public StrongNestedHit(StrongHitObject strong, DrawableDrumRollTick tick) + : base(strong, tick) + { + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (!MainObject.Judged) + return; + + ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss); + } + + public override bool OnPressed(TaikoAction action) => false; + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index bb9cd02b14..f59dc8c1ee 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -1,11 +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.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -15,17 +15,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// A list of keys which can result in hits for this HitObject. /// - protected abstract TaikoAction[] HitActions { get; } + public abstract TaikoAction[] HitActions { get; } /// - /// Whether a second hit is allowed to be processed. This occurs once this hit object has been hit successfully. + /// The action that caused this to be hit. /// - protected bool SecondHitAllowed { get; private set; } + public TaikoAction? HitAction { get; private set; } - /// - /// Whether the last key pressed is a valid hit key. - /// - private bool validKeyPressed; + private bool validActionPressed; protected DrawableHit(Hit hit) : base(hit) @@ -33,12 +30,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables FillMode = FillMode.Fit; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - AddJudgement(new TaikoJudgement { Result = HitResult.Miss }); + ApplyResult(r => r.Type = HitResult.Miss); return; } @@ -46,26 +43,33 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (result == HitResult.None) return; - if (!validKeyPressed || result == HitResult.Miss) - AddJudgement(new TaikoJudgement { Result = HitResult.Miss }); + if (!validActionPressed) + ApplyResult(r => r.Type = HitResult.Miss); else - { - AddJudgement(new TaikoJudgement - { - Result = result, - Final = !HitObject.IsStrong - }); - - SecondHitAllowed = true; - } + ApplyResult(r => r.Type = result); } public override bool OnPressed(TaikoAction action) { - validKeyPressed = HitActions.Contains(action); + if (Judged) + return false; + + validActionPressed = HitActions.Contains(action); // Only count this as handled if the new judgement is a hit - return UpdateJudgement(true); + var result = UpdateResult(true); + + if (IsHit) + HitAction = action; + + return result; + } + + public override bool OnReleased(TaikoAction action) + { + if (action == HitAction) + HitAction = null; + return base.OnReleased(action); } protected override void Update() @@ -86,8 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (State.Value) { case ArmedState.Idle: - SecondHitAllowed = false; - validKeyPressed = false; + validActionPressed = false; UnproxyContent(); this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire(); @@ -123,5 +126,65 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } } + + protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + + private class StrongNestedHit : DrawableStrongNestedHit + { + /// + /// The lenience for the second key press. + /// This does not adjust by map difficulty in ScoreV2 yet. + /// + private const double second_hit_window = 30; + + public new DrawableHit MainObject => (DrawableHit)base.MainObject; + + public StrongNestedHit(StrongHitObject strong, DrawableHit hit) + : base(strong, hit) + { + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (!MainObject.Result.HasResult) + { + base.CheckForResult(userTriggered, timeOffset); + return; + } + + if (!MainObject.Result.IsHit) + { + ApplyResult(r => r.Type = HitResult.Miss); + return; + } + + if (!userTriggered) + { + if (timeOffset > second_hit_window) + ApplyResult(r => r.Type = HitResult.Miss); + return; + } + + if (Math.Abs(MainObject.Result.TimeOffset - timeOffset) < second_hit_window) + ApplyResult(r => r.Type = HitResult.Great); + } + + public override bool OnPressed(TaikoAction action) + { + // Don't process actions until the main hitobject is hit + if (!MainObject.IsHit) + return false; + + // Don't process actions if the pressed button was released + if (MainObject.HitAction == null) + return false; + + // Don't handle invalid hit action presses + if (!MainObject.HitActions.Contains(action)) + return false; + + return UpdateResult(true); + } + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs deleted file mode 100644 index b431d35e16..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs +++ /dev/null @@ -1,103 +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.Linq; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Judgements; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public abstract class DrawableHitStrong : DrawableHit - { - /// - /// The lenience for the second key press. - /// This does not adjust by map difficulty in ScoreV2 yet. - /// - private const double second_hit_window = 30; - - private double firstHitTime; - private bool firstKeyHeld; - private TaikoAction firstHitAction; - - protected DrawableHitStrong(Hit hit) - : base(hit) - { - } - - protected override void CheckForJudgements(bool userTriggered, double timeOffset) - { - if (!SecondHitAllowed) - { - base.CheckForJudgements(userTriggered, timeOffset); - return; - } - - if (!userTriggered) - { - if (timeOffset > second_hit_window) - AddJudgement(new TaikoStrongHitJudgement { Result = HitResult.None }); - return; - } - - // If we get here, we're assured that the key pressed is the correct secondary key - - if (Math.Abs(firstHitTime - Time.Current) < second_hit_window) - AddJudgement(new TaikoStrongHitJudgement { Result = HitResult.Great }); - } - - protected override void UpdateState(ArmedState state) - { - base.UpdateState(state); - - switch (state) - { - case ArmedState.Idle: - firstHitTime = 0; - firstKeyHeld = false; - break; - } - } - - public override bool OnReleased(TaikoAction action) - { - if (action == firstHitAction) - firstKeyHeld = false; - return base.OnReleased(action); - } - - public override bool OnPressed(TaikoAction action) - { - if (AllJudged) - return false; - - // Check if we've handled the first key - if (!SecondHitAllowed) - { - // First key hasn't been handled yet, attempt to handle it - bool handled = base.OnPressed(action); - - if (handled) - { - firstHitTime = Time.Current; - firstHitAction = action; - firstKeyHeld = true; - } - - return handled; - } - - // Don't handle represses of the first key - if (firstHitAction == action) - return false; - - // Don't handle invalid hit action presses - if (!HitActions.Contains(action)) - return false; - - // Assume the intention was to hit the strong hit with both keys only if the first key is still being held down - return firstKeyHeld && UpdateJudgement(true); - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index f2194c6d56..188cafe1db 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableRimHit : DrawableHit { - protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; + public override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; public DrawableRimHit(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs deleted file mode 100644 index 728fe416f7..0000000000 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs +++ /dev/null @@ -1,26 +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.Allocation; -using osu.Game.Graphics; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; - -namespace osu.Game.Rulesets.Taiko.Objects.Drawables -{ - public class DrawableRimHitStrong : DrawableHitStrong - { - protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; - - public DrawableRimHitStrong(Hit hit) - : base(hit) - { - MainPiece.Add(new RimHitSymbolPiece()); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - MainPiece.AccentColour = colours.BlueDarker; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs new file mode 100644 index 0000000000..b27de3832a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.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.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Judgements; + +namespace osu.Game.Rulesets.Taiko.Objects.Drawables +{ + /// + /// Used as a nested hitobject to provide s for s. + /// + public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject + { + public readonly DrawableHitObject MainObject; + + protected DrawableStrongNestedHit(StrongHitObject strong, DrawableHitObject mainObject) + : base(strong) + { + MainObject = mainObject; + } + + protected override void UpdateState(ArmedState state) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 408b37e377..5059734663 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -12,23 +14,19 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableSwell : DrawableTaikoHitObject { - /// - /// A judgement is only displayed when the user has complete the swell (either a hit or miss). - /// - public override bool DisplayJudgement => AllJudged; - private const float target_ring_thick_border = 1.4f; private const float target_ring_thin_border = 1f; private const float target_ring_scale = 5f; private const float inner_ring_alpha = 0.65f; + private readonly List ticks = new List(); + private readonly Container bodyContainer; private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; @@ -106,6 +104,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables }); MainPiece.Add(symbol = new SwellSymbolPiece()); + + foreach (var tick in HitObject.NestedHitObjects.OfType()) + { + var vis = new DrawableSwellTick(tick); + + ticks.Add(vis); + AddInternal(vis); + AddNested(vis); + } } [BackgroundDependencyLoader] @@ -124,13 +131,17 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Width *= Parent.RelativeChildSize.X; } - protected override void CheckForJudgements(bool userTriggered, double timeOffset) + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (userTriggered) { - AddJudgement(new TaikoIntermediateSwellJudgement()); + var nextTick = ticks.FirstOrDefault(j => !j.IsHit); - var completion = (float)Judgements.Count / HitObject.RequiredHits; + nextTick?.TriggerResult(HitResult.Great); + + var numHits = ticks.Count(r => r.IsHit); + + var completion = (float)numHits / HitObject.RequiredHits; expandingRing .FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50) @@ -141,18 +152,30 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, Easing.OutQuint); - if (Judgements.Count == HitObject.RequiredHits) - AddJudgement(new TaikoJudgement { Result = HitResult.Great }); + if (numHits == HitObject.RequiredHits) + ApplyResult(r => r.Type = HitResult.Great); } else { if (timeOffset < 0) return; - //TODO: THIS IS SHIT AND CAN'T EXIST POST-TAIKO WORLD CUP - AddJudgement(Judgements.Count > HitObject.RequiredHits / 2 - ? new TaikoJudgement { Result = HitResult.Good } - : new TaikoJudgement { Result = HitResult.Miss }); + int numHits = 0; + + foreach (var tick in ticks) + { + if (tick.IsHit) + { + numHits++; + continue; + } + + tick.TriggerResult(HitResult.Miss); + } + + var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Good : HitResult.Miss; + + ApplyResult(r => r.Type = hitResult); } } @@ -208,7 +231,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return false; lastWasCentre = isCentre; - UpdateJudgement(true); + UpdateResult(true); return true; } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs new file mode 100644 index 0000000000..36c468c6d6 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.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.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Taiko.Objects.Drawables +{ + public class DrawableSwellTick : DrawableTaikoHitObject + { + public override bool DisplayResult => false; + + public DrawableSwellTick(TaikoHitObject hitObject) + : base(hitObject) + { + } + + public void TriggerResult(HitResult type) => ApplyResult(r => r.Type = type); + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + } + + protected override void UpdateState(ArmedState state) + { + } + + public override bool OnPressed(TaikoAction action) => false; + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index a6d61f1a5a..51e39dc648 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -101,6 +101,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Content.Add(MainPiece = CreateMainPiece()); MainPiece.KiaiMode = HitObject.Kiai; + + var strongObject = HitObject.NestedHitObjects.OfType().FirstOrDefault(); + if (strongObject != null) + { + var strongHit = CreateStrongHit(strongObject); + + AddNested(strongHit); + AddInternal(strongHit); + } } // Normal and clap samples are handled by the drum @@ -109,5 +118,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override string SampleNamespace => "Taiko"; protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(); + + /// + /// Creates the handler for this 's . + /// This is only invoked if is true for . + /// + /// The strong hitobject. + /// The strong hitobject handler. + protected virtual DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => null; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 4c9ec5473b..405ea85f0d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -54,12 +54,12 @@ namespace osu.Game.Rulesets.Taiko.Objects protected override void CreateNestedHitObjects() { - base.CreateNestedHitObjects(); - createTicks(); RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty); RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty); + + base.CreateNestedHitObjects(); } private void createTicks() diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index e546d6427f..967d5acfd7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.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 osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Taiko.Judgements; + namespace osu.Game.Rulesets.Taiko.Objects { public class DrumRollTick : TaikoHitObject @@ -20,5 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// The time allowed to hit this tick. /// public double HitWindow => TickSpacing / 2; + + public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement(); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs new file mode 100644 index 0000000000..fac3705110 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Taiko.Judgements; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public class StrongHitObject : TaikoHitObject + { + public override Judgement CreateJudgement() => new TaikoStrongJudgement(); + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index eb6f931af4..702bf63bf5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.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 osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Taiko.Objects @@ -15,5 +16,15 @@ namespace osu.Game.Rulesets.Taiko.Objects /// The number of hits required to complete the swell successfully. /// public int RequiredHits = 10; + + public override bool IsStrong { set => throw new NotSupportedException($"{nameof(Swell)} cannot be a strong hitobject."); } + + protected override void CreateNestedHitObjects() + { + base.CreateNestedHitObjects(); + + for (int i = 0; i < RequiredHits; i++) + AddNested(new SwellTick()); + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs new file mode 100644 index 0000000000..49eb6d2a15 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/SwellTick.cs @@ -0,0 +1,9 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public class SwellTick : TaikoHitObject + { + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index ffbbe28f2e..9c86b60688 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.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 osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects { @@ -26,7 +29,17 @@ namespace osu.Game.Rulesets.Taiko.Objects /// Whether this HitObject is a "strong" type. /// Strong hit objects give more points for hitting the hit object with both keys. /// - public bool IsStrong; + public virtual bool IsStrong { get; set; } + + protected override void CreateNestedHitObjects() + { + base.CreateNestedHitObjects(); + + if (IsStrong) + AddNested(new StrongHitObject { StartTime = (this as IHasEndTime)?.EndTime ?? StartTime }); + } + + public override Judgement CreateJudgement() => new TaikoJudgement(); protected override HitWindows CreateHitWindows() => new TaikoHitWindows(); } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 7dd50ab8b8..cf33141027 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.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.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; @@ -60,63 +59,31 @@ namespace osu.Game.Rulesets.Taiko.Scoring private double hpIncreaseGood; private double hpIncreaseMiss; - public TaikoScoreProcessor() - { - } - public TaikoScoreProcessor(RulesetContainer rulesetContainer) : base(rulesetContainer) { } - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void ApplyBeatmap(Beatmap beatmap) { + base.ApplyBeatmap(beatmap); + double hpMultiplierNormal = 1 / (hp_hit_great * beatmap.HitObjects.FindAll(o => o is Hit).Count * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); hpIncreaseTick = hp_hit_tick; hpIncreaseGreat = hpMultiplierNormal * hp_hit_great; hpIncreaseGood = hpMultiplierNormal * hp_hit_good; hpIncreaseMiss = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, hp_miss_min, hp_miss_mid, hp_miss_max); - - foreach (var obj in beatmap.HitObjects) - { - switch (obj) - { - case Hit _: - AddJudgement(new TaikoJudgement { Result = HitResult.Great }); - if (obj.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); - break; - case DrumRoll drumRoll: - var count = drumRoll.NestedHitObjects.OfType().Count(); - for (int i = 0; i < count; i++) - { - AddJudgement(new TaikoDrumRollTickJudgement { Result = HitResult.Great }); - - if (obj.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); - } - - AddJudgement(new TaikoJudgement { Result = HitResult.Great }); - - if (obj.IsStrong) - AddJudgement(new TaikoStrongHitJudgement()); - break; - case Swell _: - AddJudgement(new TaikoJudgement { Result = HitResult.Great }); - break; - } - } } - protected override void OnNewJudgement(Judgement judgement) + protected override void ApplyResult(JudgementResult result) { - base.OnNewJudgement(judgement); + base.ApplyResult(result); - bool isTick = judgement is TaikoDrumRollTickJudgement; + bool isTick = result.Judgement is TaikoDrumRollTickJudgement; // Apply HP changes - switch (judgement.Result) + switch (result.Type) { case HitResult.Miss: // Missing ticks shouldn't drop HP diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 609fd27bb4..7d9bc98957 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -93,13 +93,11 @@ namespace osu.Game.Rulesets.Taiko new TaikoModHidden(), new TaikoModFlashlight(), }; - case ModType.Special: + case ModType.Automation: return new Mod[] { - new TaikoModRelax(), - null, - null, new MultiMod(new TaikoModAutoplay(), new ModCinema()), + new TaikoModRelax(), }; default: return new Mod[] { }; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index b07a3ce8df..4d660918b8 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -19,16 +19,16 @@ namespace osu.Game.Rulesets.Taiko.UI /// Creates a new judgement text. /// /// The object which is being judged. - /// The judgement to visualise. - public DrawableTaikoJudgement(Judgement judgement, DrawableHitObject judgedObject) - : base(judgement, judgedObject) + /// The judgement to visualise. + public DrawableTaikoJudgement(JudgementResult result, DrawableHitObject judgedObject) + : base(result, judgedObject) { } [BackgroundDependencyLoader] private void load(OsuColour colours) { - switch (Judgement.Result) + switch (Result.Type) { case HitResult.Good: Colour = colours.GreenLight; @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected override void LoadComplete() { - if (Judgement.IsHit) + if (Result.IsHit) this.MoveToY(-100, 500); base.LoadComplete(); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 4cb8dd48a7..325beb38a5 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Taiko.UI public override void Add(DrawableHitObject h) { - h.OnJudgement += OnJudgement; + h.OnNewResult += OnNewResult; base.Add(h); @@ -224,35 +224,40 @@ namespace osu.Game.Rulesets.Taiko.UI } } - internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) + internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { if (!DisplayJudgements) return; - if (judgedObject.DisplayJudgement && judgementContainer.FirstOrDefault(j => j.JudgedObject == judgedObject) == null) - { - judgementContainer.Add(new DrawableTaikoJudgement(judgement, judgedObject) - { - Anchor = judgement.IsHit ? Anchor.TopLeft : Anchor.CentreLeft, - Origin = judgement.IsHit ? Anchor.BottomCentre : Anchor.Centre, - RelativePositionAxes = Axes.X, - X = judgement.IsHit ? judgedObject.Position.X : 0, - }); - } - - if (!judgement.IsHit) + if (!judgedObject.DisplayResult) return; - bool isRim = judgedObject.HitObject is RimHit; - - if (judgement is TaikoStrongHitJudgement) - hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == judgedObject)?.VisualiseSecondHit(); - else + switch (result.Judgement) { - hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim)); + case TaikoStrongJudgement _: + if (result.IsHit) + hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit(); + break; + default: + judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject) + { + Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft, + Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre, + RelativePositionAxes = Axes.X, + X = result.IsHit ? judgedObject.Position.X : 0, + }); - if (judgedObject.HitObject.Kiai) - kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim)); + if (!result.IsHit) + break; + + bool isRim = judgedObject.HitObject is RimHit; + + hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim)); + + if (judgedObject.HitObject.Kiai) + kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim)); + + break; } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 2fa4627bde..229ab69ceb 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -100,12 +100,8 @@ namespace osu.Game.Rulesets.Taiko.UI { switch (h) { - case CentreHit centreHit when h.IsStrong: - return new DrawableCentreHitStrong(centreHit); case CentreHit centreHit: return new DrawableCentreHit(centreHit); - case RimHit rimHit when h.IsStrong: - return new DrawableRimHitStrong(rimHit); case RimHit rimHit: return new DrawableRimHit(rimHit); case DrumRoll drumRoll: diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 400380b407..d3351f86f8 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -11,7 +11,9 @@ using osu.Game.Audio; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Skinning; namespace osu.Game.Tests.Beatmaps.Formats @@ -186,6 +188,50 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeBeatmapComboOffsetsOsu() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("hitobject-combo-offset.osu")) + using (var stream = new StreamReader(resStream)) + { + var beatmap = decoder.Decode(stream); + + var converted = new OsuBeatmapConverter(beatmap).Convert(); + new OsuBeatmapProcessor(converted).PreProcess(); + new OsuBeatmapProcessor(converted).PostProcess(); + + Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex); + Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex); + Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex); + Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex); + Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex); + Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex); + } + } + + [Test] + public void TestDecodeBeatmapComboOffsetsCatch() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("hitobject-combo-offset.osu")) + using (var stream = new StreamReader(resStream)) + { + var beatmap = decoder.Decode(stream); + + var converted = new CatchBeatmapConverter(beatmap).Convert(); + new CatchBeatmapProcessor(converted).PreProcess(); + new CatchBeatmapProcessor(converted).PostProcess(); + + Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex); + Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex); + Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex); + Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex); + Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex); + Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex); + } + } + [Test] public void TestDecodeBeatmapHitObjects() { diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 3431be91f9..82adc88c6b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -86,5 +86,19 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(78993, animation.StartTime); } } + + [Test] + public void TestDecodeVariableWithSuffix() + { + var decoder = new LegacyStoryboardDecoder(); + using (var resStream = Resource.OpenResource("variable-with-suffix.osb")) + using (var stream = new StreamReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + Assert.AreEqual(123456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X); + } + } } } diff --git a/osu.Game.Tests/Resources/hitobject-combo-offset.osu b/osu.Game.Tests/Resources/hitobject-combo-offset.osu new file mode 100644 index 0000000000..c1f0dab8e9 --- /dev/null +++ b/osu.Game.Tests/Resources/hitobject-combo-offset.osu @@ -0,0 +1,32 @@ +osu file format v14 + +[HitObjects] +// Circle with combo offset (3) +255,193,1000,49,0,0:0:0:0: +// Combo index = 4 + +// Slider with new combo followed by circle with no new combo +256,192,2000,12,0,2000,0:0:0:0: +255,193,3000,1,0,0:0:0:0: +// Combo index = 5 + +// Slider without new combo followed by circle with no new combo +256,192,4000,8,0,5000,0:0:0:0: +255,193,6000,1,0,0:0:0:0: +// Combo index = 5 + +// Slider without new combo followed by circle with new combo +256,192,7000,8,0,8000,0:0:0:0: +255,193,9000,5,0,0:0:0:0: +// Combo index = 6 + +// Slider with new combo and offset (1) followed by circle with new combo and offset (3) +256,192,10000,28,0,11000,0:0:0:0: +255,193,12000,53,0,0:0:0:0: +// Combo index = 11 + +// Slider with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo +256,192,13000,44,0,14000,0:0:0:0: +256,192,15000,8,0,16000,0:0:0:0: +255,193,17000,1,0,0:0:0:0: +// Combo index = 14 \ No newline at end of file diff --git a/osu.Game.Tests/Resources/variable-with-suffix.osb b/osu.Game.Tests/Resources/variable-with-suffix.osb new file mode 100644 index 0000000000..5c9b46ca98 --- /dev/null +++ b/osu.Game.Tests/Resources/variable-with-suffix.osb @@ -0,0 +1,5 @@ +[Variables] +$var=1234 + +[Events] +Sprite,Background,TopCentre,"img.jpg",$var56,240 diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs index 6d2b37d981..db66c01814 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs @@ -530,7 +530,7 @@ namespace osu.Game.Tests.Visual { public new List Items => base.Items; - public bool PendingFilterTask => FilterTask != null; + public bool PendingFilterTask => PendingFilter != null; } } } diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs index b232180eba..175db7d246 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using OpenTK; using osu.Framework.Allocation; @@ -116,7 +117,7 @@ namespace osu.Game.Tests.Visual private void testNullBeatmap() { - selectNullBeatmap(); + selectBeatmap(null); 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); @@ -124,28 +125,19 @@ namespace osu.Game.Tests.Visual AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any()); } - private void selectBeatmap(IBeatmap b) + private void selectBeatmap([CanBeNull] IBeatmap b) { BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null; - AddStep($"select {b.Metadata.Title} beatmap", () => + AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => { infoBefore = infoWedge.Info; - infoWedge.Beatmap = Beatmap.Value = new TestWorkingBeatmap(b); + infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : new TestWorkingBeatmap(b); }); AddUntilStep(() => infoWedge.Info != infoBefore, "wait for async load"); } - private void selectNullBeatmap() - { - AddStep("select null beatmap", () => - { - Beatmap.Value = Beatmap.Default; - infoWedge.Beatmap = Beatmap; - }); - } - private IBeatmap createTestBeatmap(RulesetInfo ruleset) { List objects = new List(); diff --git a/osu.Game.Tests/Visual/TestCaseMods.cs b/osu.Game.Tests/Visual/TestCaseMods.cs index 1a28442e38..ab53dbd968 100644 --- a/osu.Game.Tests/Visual/TestCaseMods.cs +++ b/osu.Game.Tests/Visual/TestCaseMods.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays.Mods; @@ -13,10 +12,11 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using System.Linq; using System.Collections.Generic; -using osu.Game.Rulesets.Osu; +using NUnit.Framework; +using osu.Framework.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Mania; +using osu.Game.Overlays.Mods.Sections; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.UI; using OpenTK.Graphics; @@ -36,7 +36,9 @@ namespace osu.Game.Tests.Visual typeof(ModButtonEmpty), typeof(DifficultyReductionSection), typeof(DifficultyIncreaseSection), - typeof(SpecialSection), + typeof(AutomationSection), + typeof(ConversionSection), + typeof(FunSection), }; private RulesetStore rulesets; @@ -47,11 +49,6 @@ namespace osu.Game.Tests.Visual private void load(RulesetStore rulesets) { this.rulesets = rulesets; - } - - protected override void LoadComplete() - { - base.LoadComplete(); Add(modSelect = new TestModSelectOverlay { @@ -68,34 +65,25 @@ namespace osu.Game.Tests.Visual Position = new Vector2(0, 25), }); + modDisplay.Current.UnbindBindings(); modDisplay.Current.BindTo(modSelect.SelectedMods); - AddStep("Toggle", modSelect.ToggleVisibility); - AddStep("Hide", modSelect.Hide); AddStep("Show", modSelect.Show); - - foreach (var rulesetInfo in rulesets.AvailableRulesets) - { - Ruleset ruleset = rulesetInfo.CreateInstance(); - AddStep($"switch to {ruleset.Description}", () => Ruleset.Value = rulesetInfo); - - switch (ruleset) - { - case OsuRuleset or: - testOsuMods(or); - break; - case ManiaRuleset mr: - testManiaMods(mr); - break; - } - } + AddStep("Toggle", modSelect.ToggleVisibility); + AddStep("Toggle", modSelect.ToggleVisibility); } - private void testOsuMods(OsuRuleset ruleset) + [Test] + public void TestOsuMods() { - var easierMods = ruleset.GetModsFor(ModType.DifficultyReduction); - var harderMods = ruleset.GetModsFor(ModType.DifficultyIncrease); - var assistMods = ruleset.GetModsFor(ModType.Special); + var ruleset = rulesets.AvailableRulesets.First(r => r.ID == 0); + AddStep("change ruleset", () => { Ruleset.Value = ruleset; }); + + var instance = ruleset.CreateInstance(); + + var easierMods = instance.GetModsFor(ModType.DifficultyReduction); + var harderMods = instance.GetModsFor(ModType.DifficultyIncrease); + var assistMods = instance.GetModsFor(ModType.Automation); var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail); var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); @@ -117,9 +105,40 @@ namespace osu.Game.Tests.Visual testUnimplementedMod(autoPilotMod); } - private void testManiaMods(ManiaRuleset ruleset) + [Test] + public void TestManiaMods() { - testRankedText(ruleset.GetModsFor(ModType.Special).First(m => m is ManiaModRandom)); + var ruleset = rulesets.AvailableRulesets.First(r => r.ID == 3); + AddStep("change ruleset", () => { Ruleset.Value = ruleset; }); + + testRankedText(ruleset.CreateInstance().GetModsFor(ModType.Conversion).First(m => m is ManiaModRandom)); + } + + [Test] + public void TestRulesetChanges() + { + var rulesetOsu = rulesets.AvailableRulesets.First(r => r.ID == 0); + var rulesetMania = rulesets.AvailableRulesets.First(r => r.ID == 3); + + AddStep("change ruleset to null", () => { Ruleset.Value = null; }); + + var instance = rulesetOsu.CreateInstance(); + var easierMods = instance.GetModsFor(ModType.DifficultyReduction); + var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail); + + AddStep("set mods externally", () => { modDisplay.Current.Value = new[] { noFailMod }; }); + + AddStep("change ruleset to osu", () => { Ruleset.Value = rulesetOsu; }); + + AddAssert("ensure mods still selected", () => modDisplay.Current.Value.Single(m => m is OsuModNoFail) != null); + + AddStep("change ruleset to mania", () => { Ruleset.Value = rulesetMania; }); + + AddAssert("ensure mods not selected", () => !modDisplay.Current.Value.Any(m => m is OsuModNoFail)); + + AddStep("change ruleset to osu", () => { Ruleset.Value = rulesetOsu; }); + + AddAssert("ensure mods not selected", () => !modDisplay.Current.Value.Any()); } private void testSingleMod(Mod mod) @@ -234,6 +253,8 @@ namespace osu.Game.Tests.Visual private class TestModSelectOverlay : ModSelectOverlay { + public new Bindable> SelectedMods => base.SelectedMods; + public ModButton GetModButton(Mod mod) { var section = ModSectionsContainer.Children.Single(s => s.ModType == mod.Type); diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 41c45f00f3..888bf6250f 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -8,11 +8,15 @@ using System.Linq; using System.Text; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Extensions; using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; @@ -29,6 +33,10 @@ namespace osu.Game.Tests.Visual private WorkingBeatmap defaultBeatmap; private DatabaseContextFactory factory; + [Cached] + [Cached(Type = typeof(IBindable>))] + private readonly Bindable> selectedMods = new Bindable>(new Mod[] { }); + public override IReadOnlyList RequiredTypes => new[] { typeof(SongSelect), @@ -49,11 +57,15 @@ namespace osu.Game.Tests.Visual private class TestSongSelect : PlaySongSelect { + public new Bindable Ruleset => base.Ruleset; + public WorkingBeatmap CurrentBeatmap => Beatmap.Value; public WorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap; public new BeatmapCarousel Carousel => base.Carousel; } + private TestSongSelect songSelect; + protected override void Dispose(bool isDisposing) { factory.ResetDatabase(); @@ -63,11 +75,14 @@ namespace osu.Game.Tests.Visual [BackgroundDependencyLoader] private void load() { - TestSongSelect songSelect = null; - factory = new DatabaseContextFactory(LocalStorage); factory.ResetDatabase(); + using (var usage = factory.Get()) + usage.Migrate(); + + factory.ResetDatabase(); + using (var usage = factory.Get()) usage.Migrate(); @@ -77,42 +92,35 @@ namespace osu.Game.Tests.Visual DefaultBeatmap = defaultBeatmap = Beatmap.Default }); - void loadNewSongSelect(bool deleteMaps = false) => AddStep("reload song select", () => - { - if (deleteMaps) - { - manager.Delete(manager.GetAllUsableBeatmapSets()); - Beatmap.SetDefault(); - } + Beatmap.SetDefault(); + } - if (songSelect != null) - { - Remove(songSelect); - songSelect.Dispose(); - } - - Add(songSelect = new TestSongSelect()); - }); - - loadNewSongSelect(true); - - AddWaitStep(3); + [SetUp] + public virtual void SetUp() + { + manager?.Delete(manager.GetAllUsableBeatmapSets()); + Child = songSelect = new TestSongSelect(); + } + [Test] + public void TestDummy() + { AddAssert("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap); AddAssert("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap); - AddStep("import test maps", () => - { - for (int i = 0; i < 100; i += 10) - manager.Import(createTestBeatmapSet(i)); - }); - + addManyTestMaps(); AddWaitStep(3); + AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); + } - loadNewSongSelect(); + [Test] + public void TestSorting() + { + addManyTestMaps(); AddWaitStep(3); + AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap); AddStep(@"Sort by Artist", delegate { songSelect.FilterControl.Sort = SortMode.Artist; }); @@ -121,55 +129,115 @@ namespace osu.Game.Tests.Visual AddStep(@"Sort by Difficulty", delegate { songSelect.FilterControl.Sort = SortMode.Difficulty; }); } - private BeatmapSetInfo createTestBeatmapSet(int i) + [Test] + [Ignore("needs fixing")] + public void TestImportUnderDifferentRuleset() { + changeRuleset(2); + importForRuleset(0); + AddUntilStep(() => songSelect.Carousel.SelectedBeatmap == null, "no selection"); + } + + [Test] + public void TestImportUnderCurrentRuleset() + { + changeRuleset(2); + importForRuleset(2); + importForRuleset(1); + AddUntilStep(() => songSelect.Carousel.SelectedBeatmap.RulesetID == 2, "has selection"); + + changeRuleset(1); + AddUntilStep(() => songSelect.Carousel.SelectedBeatmap.RulesetID == 1, "has selection"); + + changeRuleset(0); + AddUntilStep(() => songSelect.Carousel.SelectedBeatmap == null, "no selection"); + } + + [Test] + public void TestRulesetChangeResetsMods() + { + changeRuleset(0); + + changeMods(new OsuModHardRock()); + + int actionIndex = 0; + int modChangeIndex = 0; + int rulesetChangeIndex = 0; + + AddStep("change ruleset", () => + { + songSelect.CurrentBeatmap.Mods.ValueChanged += onModChange; + songSelect.Ruleset.ValueChanged += onRulesetChange; + + Ruleset.Value = new TaikoRuleset().RulesetInfo; + + songSelect.CurrentBeatmap.Mods.ValueChanged -= onModChange; + songSelect.Ruleset.ValueChanged -= onRulesetChange; + }); + + AddAssert("mods changed before ruleset", () => modChangeIndex < rulesetChangeIndex); + AddAssert("empty mods", () => !selectedMods.Value.Any()); + + void onModChange(IEnumerable mods) => modChangeIndex = actionIndex++; + void onRulesetChange(RulesetInfo ruleset) => rulesetChangeIndex = actionIndex--; + } + + private void importForRuleset(int id) => AddStep($"import test map for ruleset {id}", () => manager.Import(createTestBeatmapSet(getImportId(), rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray()))); + + private static int importId; + private int getImportId() => ++importId; + + private void changeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.ShortenedName))}", () => selectedMods.Value = mods); + + private void changeRuleset(int id) => AddStep($"change ruleset to {id}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == id)); + + private void addManyTestMaps() + { + AddStep("import test maps", () => + { + var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); + + for (int i = 0; i < 100; i += 10) + manager.Import(createTestBeatmapSet(i, usableRulesets)); + }); + } + + private BeatmapSetInfo createTestBeatmapSet(int setId, RulesetInfo[] rulesets) + { + int j = 0; + RulesetInfo getRuleset() => rulesets[j++ % rulesets.Length]; + + var beatmaps = new List(); + + for (int i = 0; i < 6; i++) + { + int beatmapId = setId * 10 + i; + + beatmaps.Add(new BeatmapInfo + { + Ruleset = getRuleset(), + OnlineBeatmapID = beatmapId, + Path = "normal.osu", + Version = $"{beatmapId}", + BaseDifficulty = new BeatmapDifficulty + { + OverallDifficulty = 3.5f, + } + }); + } + return new BeatmapSetInfo { - OnlineBeatmapSetID = 1234 + i, + OnlineBeatmapSetID = setId, Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), Metadata = new BeatmapMetadata { // 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), + Artist = "Some Artist " + RNG.Next(0, 9), + Title = $"Some Song (set id {setId})", AuthorString = "Some Guy " + RNG.Next(0, 9), }, - Beatmaps = new List(new[] - { - new BeatmapInfo - { - OnlineBeatmapID = 1234 + i, - Ruleset = rulesets.AvailableRulesets.First(), - Path = "normal.osu", - Version = "Normal", - BaseDifficulty = new BeatmapDifficulty - { - OverallDifficulty = 3.5f, - } - }, - new BeatmapInfo - { - OnlineBeatmapID = 1235 + i, - Ruleset = rulesets.AvailableRulesets.First(), - Path = "hard.osu", - Version = "Hard", - BaseDifficulty = new BeatmapDifficulty - { - OverallDifficulty = 5, - } - }, - new BeatmapInfo - { - OnlineBeatmapID = 1236 + i, - Ruleset = rulesets.AvailableRulesets.First(), - Path = "insane.osu", - Version = "Insane", - BaseDifficulty = new BeatmapDifficulty - { - OverallDifficulty = 7, - } - }, - }), + Beatmaps = beatmaps }; } } diff --git a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs index 52a9db080d..de839a21af 100644 --- a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs +++ b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs @@ -1,25 +1,61 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Threading; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { - public class TestCasePlayerLoader : OsuTestCase + public class TestCasePlayerLoader : ManualInputManagerTestCase { + private PlayerLoader loader; + [BackgroundDependencyLoader] private void load(OsuGameBase game) { Beatmap.Value = new DummyWorkingBeatmap(game); - AddStep("load dummy beatmap", () => Add(new PlayerLoader(new Player + AddStep("load dummy beatmap", () => Add(loader = new PlayerLoader(new Player { AllowPause = false, AllowLeadIn = false, AllowResults = false, }))); + + AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); + + AddUntilStep(() => !loader.IsCurrentScreen, "wait for no longer current"); + + AddStep("load slow dummy beatmap", () => + { + SlowLoadPlayer slow; + + Add(loader = new PlayerLoader(slow = new SlowLoadPlayer + { + AllowPause = false, + AllowLeadIn = false, + AllowResults = false, + })); + + Scheduler.AddDelayed(() => slow.Ready = true, 5000); + }); + + AddUntilStep(() => !loader.IsCurrentScreen, "wait for no longer current"); + } + + protected class SlowLoadPlayer : Player + { + public bool Ready; + + [BackgroundDependencyLoader] + private void load() + { + while (!Ready) + Thread.Sleep(1); + } } } + } diff --git a/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs b/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs index 83bbbfddd1..b70117a3ad 100644 --- a/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs +++ b/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs @@ -54,11 +54,11 @@ namespace osu.Game.Tests.Visual breadcrumbs.Current.TriggerChange(); - assertCurrent(); + waitForCurrent(); pushNext(); - assertCurrent(); + waitForCurrent(); pushNext(); - assertCurrent(); + waitForCurrent(); AddStep(@"make start current", () => { @@ -66,8 +66,9 @@ namespace osu.Game.Tests.Visual currentScreen = startScreen; }); - assertCurrent(); + waitForCurrent(); pushNext(); + waitForCurrent(); AddAssert(@"only 2 items", () => breadcrumbs.Items.Count() == 2); AddStep(@"exit current", () => changedScreen.Exit()); AddAssert(@"current screen is first", () => startScreen == changedScreen); @@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual } private void pushNext() => AddStep(@"push next screen", () => currentScreen = ((TestScreen)currentScreen).PushNext()); - private void assertCurrent() => AddAssert(@"changedScreen correct", () => currentScreen == changedScreen); + private void waitForCurrent() => AddUntilStep(() => currentScreen.IsCurrentScreen, "current screen"); private abstract class TestScreen : OsuScreen { diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 532915100b..d638af0c38 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,8 +1,14 @@  + + + + + + WinExe - netcoreapp2.1;net471 + netcoreapp2.1 diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 67f02c8ac4..1c28b533d2 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -329,7 +329,7 @@ namespace osu.Game.Beatmaps return; } - await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); + await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs").Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning); } /// diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs index 0173125e8b..9d7cd673dc 100644 --- a/osu.Game/Beatmaps/BeatmapProcessor.cs +++ b/osu.Game/Beatmaps/BeatmapProcessor.cs @@ -27,11 +27,10 @@ namespace osu.Game.Beatmaps if (obj.NewCombo) { obj.IndexInCurrentCombo = 0; + obj.ComboIndex = (lastObj?.ComboIndex ?? 0) + obj.ComboOffset + 1; + if (lastObj != null) - { lastObj.LastInCombo = true; - obj.ComboIndex = lastObj.ComboIndex + 1; - } } else if (lastObj != null) { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 26f28c86ca..31a7698f50 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -126,16 +126,16 @@ namespace osu.Game.Beatmaps.Formats switch (beatmap.BeatmapInfo.RulesetID) { case 0: - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); + parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); break; case 1: - parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(); + parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion); break; case 2: - parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(); + parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion); break; case 3: - parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(); + parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion); break; } @@ -354,6 +354,11 @@ namespace osu.Game.Beatmaps.Formats private void handleTimingControlPoint(TimingControlPoint newPoint) { + var existing = beatmap.ControlPointInfo.TimingPointAt(newPoint.Time); + + if (existing.Time == newPoint.Time) + beatmap.ControlPointInfo.TimingPoints.Remove(existing); + beatmap.ControlPointInfo.TimingPoints.Add(newPoint); } @@ -364,7 +369,9 @@ namespace osu.Game.Beatmaps.Formats if (newPoint.EquivalentTo(existing)) return; - beatmap.ControlPointInfo.DifficultyPoints.RemoveAll(x => x.Time == newPoint.Time); + if (existing.Time == newPoint.Time) + beatmap.ControlPointInfo.DifficultyPoints.Remove(existing); + beatmap.ControlPointInfo.DifficultyPoints.Add(newPoint); } @@ -375,6 +382,9 @@ namespace osu.Game.Beatmaps.Formats if (newPoint.EquivalentTo(existing)) return; + if (existing.Time == newPoint.Time) + beatmap.ControlPointInfo.EffectPoints.Remove(existing); + beatmap.ControlPointInfo.EffectPoints.Add(newPoint); } @@ -385,6 +395,9 @@ namespace osu.Game.Beatmaps.Formats if (newPoint.EquivalentTo(existing)) return; + if (existing.Time == newPoint.Time) + beatmap.ControlPointInfo.SamplePoints.Remove(existing); + beatmap.ControlPointInfo.SamplePoints.Add(newPoint); } @@ -392,9 +405,9 @@ namespace osu.Game.Beatmaps.Formats { // If the ruleset wasn't specified, assume the osu!standard ruleset. if (parser == null) - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); + parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion); - var obj = parser.Parse(line, getOffsetTime()); + var obj = parser.Parse(line); if (obj != null) { diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 76a3d75e36..e9f37e583b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps.Formats if (ShouldSkipLine(line)) continue; - if (line.StartsWith(@"[") && line.EndsWith(@"]")) + if (line.StartsWith(@"[", StringComparison.Ordinal) && line.EndsWith(@"]", StringComparison.Ordinal)) { if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) { @@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps.Formats } } - protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.StartsWith("//"); + protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.StartsWith("//", StringComparison.Ordinal); protected virtual void ParseLine(T output, Section section, string line) { diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index b418cbd5ec..1063dfc923 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats private void handleEvents(string line) { var depth = 0; - while (line.StartsWith(" ") || line.StartsWith("_")) + while (line.StartsWith(" ", StringComparison.Ordinal) || line.StartsWith("_", StringComparison.Ordinal)) { ++depth; line = line.Substring(1); @@ -289,15 +289,10 @@ namespace osu.Game.Beatmaps.Formats while (line.IndexOf('$') >= 0) { string origLine = line; - string[] split = line.Split(','); - for (int i = 0; i < split.Length; i++) - { - var item = split[i]; - if (item.StartsWith("$") && variables.ContainsKey(item)) - split[i] = variables[item]; - } - line = string.Join(",", split); + foreach (var v in variables) + line = line.Replace(v.Key, v.Value); + if (line == origLine) break; } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index cfd34d44e7..6c906bb1e4 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -134,6 +134,8 @@ namespace osu.Game.Beatmaps return converted; } + public override string ToString() => BeatmapInfo.ToString(); + public bool BackgroundLoaded => background.IsResultAvailable; public Texture Background => background.Value.Result; public async Task GetBackgroundAsync() => await background.Value; diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 0465c0ad73..ac79a8f565 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -198,6 +198,8 @@ namespace osu.Game.Database try { + Logger.Log($"Importing {item}...", LoggingTarget.Database); + using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. { try @@ -412,7 +414,7 @@ namespace osu.Game.Database private ArchiveReader getReaderFrom(string path) { if (ZipUtils.IsZipArchive(path)) - return new ZipArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path)); + return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), Path.GetFileName(path)); if (Directory.Exists(path)) return new LegacyFilesystemReader(path); throw new InvalidFormatException($"{path} is not a valid archive"); diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 7b3337cb23..bc30794298 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Drawing.Imaging; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -19,6 +18,7 @@ using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using SixLabors.ImageSharp; namespace osu.Game.Graphics { @@ -90,7 +90,7 @@ namespace osu.Game.Graphics waitDelegate.Cancel(); } - using (var bitmap = await host.TakeScreenshotAsync()) + using (var image = await host.TakeScreenshotAsync()) { Interlocked.Decrement(ref screenShotTasks); @@ -102,10 +102,10 @@ namespace osu.Game.Graphics switch (screenshotFormat.Value) { case ScreenshotFormat.Png: - bitmap.Save(stream, ImageFormat.Png); + image.SaveAsPng(stream); break; case ScreenshotFormat.Jpg: - bitmap.Save(stream, ImageFormat.Jpeg); + image.SaveAsJpeg(stream); break; default: throw new ArgumentOutOfRangeException(nameof(screenshotFormat)); diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 1b91d0cad3..e2a0b88b2a 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -37,7 +37,7 @@ namespace osu.Game.Graphics.UserInterface { TabContainer.Spacing = new Vector2(10f, 0f); - Add(strip = new Box + AddInternal(strip = new Box { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, diff --git a/osu.Game/Online/Chat/InfoMessage.cs b/osu.Game/Online/Chat/InfoMessage.cs index 2be025e403..2ff901deb1 100644 --- a/osu.Game/Online/Chat/InfoMessage.cs +++ b/osu.Game/Online/Chat/InfoMessage.cs @@ -6,7 +6,7 @@ using osu.Game.Users; namespace osu.Game.Online.Chat { - public class InfoMessage : Message + public class InfoMessage : LocalMessage { private static int infoID = -1; diff --git a/osu.Game/Online/Chat/LocalEchoMessage.cs b/osu.Game/Online/Chat/LocalEchoMessage.cs index 2e90b9d3fd..7d678029aa 100644 --- a/osu.Game/Online/Chat/LocalEchoMessage.cs +++ b/osu.Game/Online/Chat/LocalEchoMessage.cs @@ -3,7 +3,7 @@ namespace osu.Game.Online.Chat { - public class LocalEchoMessage : Message + public class LocalEchoMessage : LocalMessage { public LocalEchoMessage() : base(null) { diff --git a/osu.Game/Online/Chat/LocalMessage.cs b/osu.Game/Online/Chat/LocalMessage.cs new file mode 100644 index 0000000000..93f1e7f9ea --- /dev/null +++ b/osu.Game/Online/Chat/LocalMessage.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Online.Chat +{ + /// + /// A message which is generated and displayed locally. + /// + public class LocalMessage : Message + { + protected LocalMessage(long? id) + : base(id) + { + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 83912944d4..94678a9dde 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -36,6 +36,8 @@ using osu.Game.Skinning; using OpenTK.Graphics; using osu.Game.Overlays.Volume; using osu.Game.Screens.Select; +using osu.Game.Utils; +using LogLevel = osu.Framework.Logging.LogLevel; namespace osu.Game { @@ -65,6 +67,8 @@ namespace osu.Game private ScreenshotManager screenshotManager; + protected RavenLogger RavenLogger; + public virtual Storage GetStorageForStableInstall() => null; private Intro intro @@ -99,13 +103,17 @@ namespace osu.Game 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()); + [Cached] + [Cached(Type = typeof(IBindable>))] + private readonly Bindable> selectedMods = new Bindable>(new Mod[] { }); public OsuGame(string[] args = null) { this.args = args; forwardLoggedErrorsToNotifications(); + + RavenLogger = new RavenLogger(this); } public void ToggleSettings() => settings.ToggleVisibility(); @@ -143,13 +151,15 @@ namespace osu.Game if (args?.Length > 0) { - var paths = args.Where(a => !a.StartsWith(@"-")); - - Task.Run(() => Import(paths.ToArray())); + var paths = args.Where(a => !a.StartsWith(@"-")).ToArray(); + if (paths.Length > 0) + Task.Run(() => Import(paths)); } dependencies.CacheAs(this); + dependencies.Cache(RavenLogger); + dependencies.CacheAs(ruleset); dependencies.CacheAs>(ruleset); @@ -197,7 +207,13 @@ namespace osu.Game return; } - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(beatmap.Beatmaps.First()); + var databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID); + + // Use first beatmap available for current ruleset, else switch ruleset. + var first = databasedSet.Beatmaps.FirstOrDefault(b => b.Ruleset == ruleset.Value) ?? databasedSet.Beatmaps.First(); + + ruleset.Value = first.Ruleset; + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first); } switch (currentScreen) @@ -265,6 +281,12 @@ namespace osu.Game menu.Push(new PlayerLoader(new ReplayPlayer(s.Replay))); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + RavenLogger.Dispose(); + } + protected override void LoadComplete() { // this needs to be cached before base.LoadComplete as it is used by MenuCursorContainer. @@ -441,7 +463,7 @@ namespace osu.Game Schedule(() => notifications.Post(new SimpleNotification { Icon = entry.Level == LogLevel.Important ? FontAwesome.fa_exclamation_circle : FontAwesome.fa_bomb, - Text = entry.Message, + Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), })); } else if (recentLogCount == short_term_display_limit) @@ -482,7 +504,25 @@ namespace osu.Game // schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached). // with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile, // we could avoid the need for scheduling altogether. - Schedule(() => { asyncLoadStream = asyncLoadStream?.ContinueWith(t => LoadComponentAsync(d, add).Wait()) ?? LoadComponentAsync(d, add); }); + Schedule(() => + { + if (asyncLoadStream != null) + { + //chain with existing load stream + asyncLoadStream = asyncLoadStream.ContinueWith(async t => + { + try + { + await LoadComponentAsync(d, add); + } + catch (OperationCanceledException) + { + } + }); + } + else + asyncLoadStream = LoadComponentAsync(d, add); + }); } public bool OnPressed(GlobalAction action) @@ -593,6 +633,7 @@ namespace osu.Game private void screenAdded(Screen newScreen) { currentScreen = (OsuScreen)newScreen; + Logger.Log($"Screen changed → {currentScreen}"); newScreen.ModePushed += screenAdded; newScreen.Exited += screenRemoved; @@ -601,6 +642,7 @@ namespace osu.Game private void screenRemoved(Screen newScreen) { currentScreen = (OsuScreen)newScreen; + Logger.Log($"Screen changed ← {currentScreen}"); if (newScreen == null) Exit(); diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs index 223ca1c904..f3e49f68b1 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs @@ -1,18 +1,28 @@ // 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; +using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Users; using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Overlays.BeatmapSet.Buttons { - public class DownloadButton : HeaderButton + public class DownloadButton : HeaderButton, IHasTooltip { + public string TooltipText => Enabled ? null : "You gotta be an osu!supporter to download for now 'yo"; + + private readonly IBindable localUser = new Bindable(); + public DownloadButton(BeatmapSetInfo set, bool noVideo = false) { Width = 120; @@ -86,5 +96,17 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons } }; } + + [BackgroundDependencyLoader] + private void load(APIAccess api) + { + localUser.BindTo(api.LocalUser); + localUser.BindValueChanged(userChanged, true); + Enabled.BindValueChanged(enabledChanged, true); + } + + private void userChanged(User user) => Enabled.Value = user.IsSupporter; + + private void enabledChanged(bool enabled) => this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint); } } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index bcc8879902..c57e71b5ad 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat if (!IsLoaded) return; - if (scroll.IsScrolledToEnd(10) || !flow.Children.Any()) + if (scroll.IsScrolledToEnd(10) || !flow.Children.Any() || newMessages.Any(m => m is LocalMessage)) scrollToEnd(); var staleMessages = flow.Children.Where(c => c.LifetimeEnd == double.MaxValue).ToArray(); diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index 45e1164a57..850ead37f6 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics.Sprites; using osu.Framework.Allocation; using osu.Framework.Localisation; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.States; using osu.Game.Beatmaps; namespace osu.Game.Overlays.Direct @@ -26,12 +25,14 @@ namespace osu.Game.Overlays.Direct private PlayButton playButton; private Box progressBar; - private Container downloadContainer; + + protected override bool FadePlayButton => false; protected override PlayButton PlayButton => playButton; protected override Box PreviewBar => progressBar; - public DirectListPanel(BeatmapSetInfo beatmap) : base(beatmap) + public DirectListPanel(BeatmapSetInfo beatmap) + : base(beatmap) { RelativeSizeAxes = Axes.X; Height = height; @@ -66,30 +67,45 @@ namespace osu.Game.Overlays.Direct Spacing = new Vector2(10, 0), Children = new Drawable[] { - playButton = new PlayButton(SetInfo) - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Size = new Vector2(height / 2), - FillMode = FillMode.Fit, - Alpha = 0, - }, new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + new FillFlowContainer { - Current = localisation.GetUnicodePreference(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), - TextSize = 18, - Font = @"Exo2.0-BoldItalic", - }, - new OsuSpriteText - { - Current = localisation.GetUnicodePreference(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), - Font = @"Exo2.0-BoldItalic", + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + playButton = new PlayButton(SetInfo) + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Size = new Vector2(height / 2), + FillMode = FillMode.Fit, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Current = localisation.GetUnicodePreference(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), + TextSize = 18, + Font = @"Exo2.0-BoldItalic", + }, + new OsuSpriteText + { + Current = localisation.GetUnicodePreference(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), + Font = @"Exo2.0-BoldItalic", + }, + } + }, + } }, new FillFlowContainer { @@ -108,16 +124,13 @@ namespace osu.Game.Overlays.Direct Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - LayoutEasing = Easing.OutQuint, - LayoutDuration = transition_duration, Children = new Drawable[] { - downloadContainer = new Container + new Container { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, - Alpha = 0, Child = new DownloadButton(SetInfo) { Size = new Vector2(height - vertical_padding * 3), @@ -184,17 +197,5 @@ 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 7d5c0c16cc..322db0b7d6 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -40,6 +40,8 @@ namespace osu.Game.Overlays.Direct protected abstract PlayButton PlayButton { get; } protected abstract Box PreviewBar { get; } + protected virtual bool FadePlayButton => true; + protected override Container Content => content; protected DirectPanel(BeatmapSetInfo setInfo) @@ -125,7 +127,8 @@ namespace osu.Game.Overlays.Direct { content.TweenEdgeEffectTo(edgeEffectHovered, hover_transition_time, Easing.OutQuint); content.MoveToY(-4, hover_transition_time, Easing.OutQuint); - PlayButton.FadeIn(120, Easing.InOutQuint); + if (FadePlayButton) + PlayButton.FadeIn(120, Easing.InOutQuint); return base.OnHover(state); } @@ -134,7 +137,7 @@ namespace osu.Game.Overlays.Direct { content.TweenEdgeEffectTo(edgeEffectNormal, hover_transition_time, Easing.OutQuint); content.MoveToY(0, hover_transition_time, Easing.OutQuint); - if (!PreviewPlaying) + if (FadePlayButton && !PreviewPlaying) PlayButton.FadeOut(120, Easing.InOutQuint); base.OnHoverLost(state); diff --git a/osu.Game/Overlays/Direct/DownloadButton.cs b/osu.Game/Overlays/Direct/DownloadButton.cs index 7758e171c5..99a5881487 100644 --- a/osu.Game/Overlays/Direct/DownloadButton.cs +++ b/osu.Game/Overlays/Direct/DownloadButton.cs @@ -14,6 +14,7 @@ namespace osu.Game.Overlays.Direct { public class DownloadButton : OsuAnimatedButton { + private readonly BeatmapSetInfo beatmapSet; private readonly SpriteIcon icon; private readonly SpriteIcon checkmark; private readonly BeatmapSetDownloader downloader; @@ -21,11 +22,13 @@ namespace osu.Game.Overlays.Direct private OsuColour colours; - public DownloadButton(BeatmapSetInfo set, bool noVideo = false) + public DownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false) { + this.beatmapSet = beatmapSet; + AddRange(new Drawable[] { - downloader = new BeatmapSetDownloader(set, noVideo), + downloader = new BeatmapSetDownloader(beatmapSet, noVideo), background = new Box { RelativeSizeAxes = Axes.Both, @@ -47,26 +50,6 @@ namespace osu.Game.Overlays.Direct Icon = FontAwesome.fa_check, } }); - - Action = () => - { - if (downloader.DownloadState == BeatmapSetDownloader.DownloadStatus.Downloading) - { - // todo: replace with ShakeContainer after https://github.com/ppy/osu/pull/2909 is merged. - Content.MoveToX(-5, 50, Easing.OutSine).Then() - .MoveToX(5, 100, Easing.InOutSine).Then() - .MoveToX(-5, 100, Easing.InOutSine).Then() - .MoveToX(0, 50, Easing.InSine); - } - else if (downloader.DownloadState == BeatmapSetDownloader.DownloadStatus.Downloaded) - { - // TODO: Jump to song select with this set when the capability is implemented - } - else - { - downloader.Download(); - } - }; } protected override void LoadComplete() @@ -77,9 +60,29 @@ namespace osu.Game.Overlays.Direct } [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours) + private void load(OsuColour colours, OsuGame game) { this.colours = colours; + + Action = () => + { + switch (downloader.DownloadState.Value) + { + case BeatmapSetDownloader.DownloadStatus.Downloading: + // todo: replace with ShakeContainer after https://github.com/ppy/osu/pull/2909 is merged. + Content.MoveToX(-5, 50, Easing.OutSine).Then() + .MoveToX(5, 100, Easing.InOutSine).Then() + .MoveToX(-5, 100, Easing.InOutSine).Then() + .MoveToX(0, 50, Easing.InSine); + break; + case BeatmapSetDownloader.DownloadStatus.Downloaded: + game.PresentBeatmap(beatmapSet); + break; + default: + downloader.Download(); + break; + } + }; } private void updateState(BeatmapSetDownloader.DownloadStatus state) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 2fb44bb927..37bffaaf12 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using OpenTK; -using OpenTK.Graphics; using OpenTK.Input; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -45,7 +44,6 @@ namespace osu.Game.Overlays.Mods return new ModButton(m) { - SelectedColour = selectedColour, SelectionChanged = Action, }; }).ToArray(); @@ -57,25 +55,14 @@ namespace osu.Game.Overlays.Mods private ModButton[] buttons = { }; - private Color4 selectedColour = Color4.White; - public Color4 SelectedColour - { - get => selectedColour; - set - { - if (value == selectedColour) return; - selectedColour = value; - - foreach (ModButton button in buttons) - button.SelectedColour = value; - } - } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - var index = Array.IndexOf(ToggleKeys, args.Key); - if (index > -1 && index < buttons.Length) - buttons[index].SelectNext(state.Keyboard.ShiftPressed ? -1 : 1); + if (ToggleKeys != null) + { + var index = Array.IndexOf(ToggleKeys, args.Key); + if (index > -1 && index < buttons.Length) + buttons[index].SelectNext(state.Keyboard.ShiftPressed ? -1 : 1); + } return base.OnKeyDown(state, args); } @@ -125,6 +112,10 @@ namespace osu.Game.Overlays.Mods protected ModSection() { AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + + Origin = Anchor.TopCentre; + Anchor = Anchor.TopCentre; Children = new Drawable[] { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 98cf111ba0..e83dedaf35 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -21,11 +21,17 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; using osu.Game.Rulesets; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Mods.Sections; namespace osu.Game.Overlays.Mods { public class ModSelectOverlay : WaveOverlayContainer { + /// + /// How much this container should overflow the sides of the screen to account for parallax shifting. + /// + private const float overflow_padding = 50; + private const float content_width = 0.8f; protected Color4 LowMultiplierColour, HighMultiplierColour; @@ -38,9 +44,39 @@ namespace osu.Game.Overlays.Mods protected readonly FillFlowContainer ModSectionsContainer; - public readonly Bindable> SelectedMods = new Bindable>(); + protected readonly Bindable> SelectedMods = new Bindable>(new Mod[] { }); - public readonly IBindable Ruleset = new Bindable(); + protected readonly IBindable Ruleset = new Bindable(); + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, IBindable ruleset, AudioManager audio, Bindable> selectedMods) + { + LowMultiplierColour = colours.Red; + HighMultiplierColour = colours.Green; + UnrankedLabel.Colour = colours.Blue; + + Ruleset.BindTo(ruleset); + if (selectedMods != null) SelectedMods.BindTo(selectedMods); + + sampleOn = audio.Sample.Get(@"UI/check-on"); + sampleOff = audio.Sample.Get(@"UI/check-off"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Ruleset.BindValueChanged(rulesetChanged, true); + SelectedMods.BindValueChanged(selectedModsChanged, true); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + Ruleset.UnbindAll(); + SelectedMods.UnbindAll(); + } private void rulesetChanged(RulesetInfo newRuleset) { @@ -50,33 +86,16 @@ namespace osu.Game.Overlays.Mods foreach (ModSection section in ModSectionsContainer.Children) section.Mods = instance.GetModsFor(section.ModType); + + // attempt to re-select any already selected mods. + // this may be the first time we are receiving the ruleset, in which case they will still match. + selectedModsChanged(SelectedMods.Value); + + // write the mods back to the SelectedMods bindable in the case a change was not applicable. + // this generally isn't required as the previous line will perform deselection; just here for safety. refreshSelectedMods(); } - [BackgroundDependencyLoader] - private void load(OsuColour colours, IBindable ruleset, AudioManager audio) - { - SelectedMods.ValueChanged += selectedModsChanged; - - LowMultiplierColour = colours.Red; - HighMultiplierColour = colours.Green; - UnrankedLabel.Colour = colours.Blue; - - Ruleset.BindTo(ruleset); - Ruleset.BindValueChanged(rulesetChanged, true); - - sampleOn = audio.Sample.Get(@"UI/check-on"); - sampleOff = audio.Sample.Get(@"UI/check-off"); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - Ruleset.UnbindAll(); - SelectedMods.UnbindAll(); - } - private void selectedModsChanged(IEnumerable obj) { foreach (ModSection section in ModSectionsContainer.Children) @@ -175,10 +194,7 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } - private void refreshSelectedMods() - { - SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); - } + private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); public ModSelectOverlay() { @@ -188,6 +204,12 @@ namespace osu.Game.Overlays.Mods Waves.FourthWaveColour = OsuColour.FromHex(@"003a4e"); Height = 510; + Padding = new MarginPadding + { + Left = -overflow_padding, + Right = -overflow_padding + }; + Children = new Drawable[] { new Container @@ -211,178 +233,184 @@ namespace osu.Game.Overlays.Mods }, }, }, - new FillFlowContainer + new GridContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0f, 10f), - Children = new Drawable[] + RowDimensions = new[] { - // Header - new Container + new Dimension(GridSizeMode.Absolute, 90), + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.Absolute, 70), + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.X, - Height = 82, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Children = new Drawable[] + new Container { - new Box + RelativeSizeAxes = Axes.Both, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(10).Opacity(100), - }, - new FillFlowContainer - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Width = content_width, - Padding = new MarginPadding + new Box { - Top = 10, - Bottom = 10, + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(10).Opacity(100), }, - Children = new Drawable[] + new FillFlowContainer { - new OsuSpriteText + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Width = content_width, + Padding = new MarginPadding { - Font = @"Exo2.0-Bold", - Text = @"Gameplay Mods", - TextSize = 22, - Shadow = true, - Margin = new MarginPadding + Left = overflow_padding, + Right = overflow_padding + }, + Children = new Drawable[] + { + new OsuSpriteText { - Bottom = 4, + Font = @"Exo2.0-Bold", + Text = @"Gameplay Mods", + TextSize = 22, + Shadow = true, + Margin = new MarginPadding + { + Bottom = 4, + }, + }, + new OsuTextFlowContainer(text => + { + text.TextSize = 18; + text.Shadow = true; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play.\nOthers are just for fun.", }, - }, - new OsuSpriteText - { - Text = @"Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play.", - TextSize = 18, - Shadow = true, - }, - new OsuSpriteText - { - Text = @"Others are just for fun.", - TextSize = 18, - Shadow = true, }, }, }, }, }, - // Body - ModSectionsContainer = new FillFlowContainer + new Drawable[] { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 10f), - Width = content_width, - Children = new ModSection[] + // Body + new OsuScrollContainer { - new DifficultyReductionSection + ScrollbarVisible = false, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Vertical = 10, + Left = overflow_padding, + Right = overflow_padding + }, + Child = ModSectionsContainer = new FillFlowContainer { - RelativeSizeAxes = Axes.X, Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, - Action = modButtonPressed, - }, - new DifficultyIncreaseSection - { RelativeSizeAxes = Axes.X, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Action = modButtonPressed, - }, - new SpecialSection - { - RelativeSizeAxes = Axes.X, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Action = modButtonPressed, - }, - } - }, - // Footer - new Container - { - RelativeSizeAxes = Axes.X, - Height = 70, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = new Color4(172, 20, 116, 255), - Alpha = 0.5f, - }, - footerContainer = new FillFlowContainer - { - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, + Spacing = new Vector2(0f, 10f), Width = content_width, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding + Children = new ModSection[] { - Vertical = 15 + new DifficultyReductionSection { Action = modButtonPressed }, + new DifficultyIncreaseSection { Action = modButtonPressed }, + new AutomationSection { Action = modButtonPressed }, + new ConversionSection { Action = modButtonPressed }, + new FunSection { Action = modButtonPressed }, + } + }, + }, + }, + new Drawable[] + { + // Footer + new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(172, 20, 116, 255), + Alpha = 0.5f, }, - Children = new Drawable[] + footerContainer = new FillFlowContainer { - DeselectAllButton = new TriangleButton + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Width = content_width, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { - Width = 180, - Text = "Deselect All", - Action = DeselectAll, - Margin = new MarginPadding - { - Right = 20 - } + Vertical = 15, + Left = overflow_padding, + Right = overflow_padding }, - new OsuSpriteText + Children = new Drawable[] { - Text = @"Score Multiplier:", - TextSize = 30, - Margin = new MarginPadding + DeselectAllButton = new TriangleButton { - Top = 5, - Right = 10 - } - }, - MultiplierLabel = new OsuSpriteText - { - Font = @"Exo2.0-Bold", - TextSize = 30, - Margin = new MarginPadding + Width = 180, + Text = "Deselect All", + Action = DeselectAll, + Margin = new MarginPadding + { + Right = 20 + } + }, + new OsuSpriteText { - Top = 5 - } - }, - UnrankedLabel = new OsuSpriteText - { - Font = @"Exo2.0-Bold", - Text = @"(Unranked)", - TextSize = 30, - Margin = new MarginPadding + Text = @"Score Multiplier:", + TextSize = 30, + Margin = new MarginPadding + { + Top = 5, + Right = 10 + } + }, + MultiplierLabel = new OsuSpriteText { - Top = 5, - Left = 10 + Font = @"Exo2.0-Bold", + TextSize = 30, + 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/Mods/Sections/AutomationSection.cs b/osu.Game/Overlays/Mods/Sections/AutomationSection.cs new file mode 100644 index 0000000000..2b509d539e --- /dev/null +++ b/osu.Game/Overlays/Mods/Sections/AutomationSection.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; +using OpenTK.Input; + +namespace osu.Game.Overlays.Mods.Sections +{ + public class AutomationSection : ModSection + { + protected override Key[] ToggleKeys => new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }; + public override ModType ModType => ModType.Automation; + + public AutomationSection() + { + Header = @"Automation"; + } + } +} diff --git a/osu.Game/Overlays/Mods/Sections/ConversionSection.cs b/osu.Game/Overlays/Mods/Sections/ConversionSection.cs new file mode 100644 index 0000000000..568f0ecfce --- /dev/null +++ b/osu.Game/Overlays/Mods/Sections/ConversionSection.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; +using OpenTK.Input; + +namespace osu.Game.Overlays.Mods.Sections +{ + public class ConversionSection : ModSection + { + protected override Key[] ToggleKeys => null; + public override ModType ModType => ModType.Conversion; + + public ConversionSection() + { + Header = @"Conversion"; + } + } +} diff --git a/osu.Game/Overlays/Mods/DifficultyIncreaseSection.cs b/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs similarity index 70% rename from osu.Game/Overlays/Mods/DifficultyIncreaseSection.cs rename to osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs index d7d9a90e77..5aced7ed5d 100644 --- a/osu.Game/Overlays/Mods/DifficultyIncreaseSection.cs +++ b/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs @@ -1,24 +1,16 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Input; -using osu.Framework.Allocation; -using osu.Game.Graphics; using osu.Game.Rulesets.Mods; +using OpenTK.Input; -namespace osu.Game.Overlays.Mods +namespace osu.Game.Overlays.Mods.Sections { public class DifficultyIncreaseSection : ModSection { protected override Key[] ToggleKeys => new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }; public override ModType ModType => ModType.DifficultyIncrease; - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - SelectedColour = colours.YellowLight; - } - public DifficultyIncreaseSection() { Header = @"Difficulty Increase"; diff --git a/osu.Game/Overlays/Mods/DifficultyReductionSection.cs b/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs similarity index 70% rename from osu.Game/Overlays/Mods/DifficultyReductionSection.cs rename to osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs index 013deea579..29fae2c70a 100644 --- a/osu.Game/Overlays/Mods/DifficultyReductionSection.cs +++ b/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs @@ -1,24 +1,16 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Input; -using osu.Framework.Allocation; -using osu.Game.Graphics; using osu.Game.Rulesets.Mods; +using OpenTK.Input; -namespace osu.Game.Overlays.Mods +namespace osu.Game.Overlays.Mods.Sections { public class DifficultyReductionSection : ModSection { protected override Key[] ToggleKeys => new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }; public override ModType ModType => ModType.DifficultyReduction; - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - SelectedColour = colours.GreenLight; - } - public DifficultyReductionSection() { Header = @"Difficulty Reduction"; diff --git a/osu.Game/Overlays/Mods/Sections/FunSection.cs b/osu.Game/Overlays/Mods/Sections/FunSection.cs new file mode 100644 index 0000000000..ef975d9d75 --- /dev/null +++ b/osu.Game/Overlays/Mods/Sections/FunSection.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; +using OpenTK.Input; + +namespace osu.Game.Overlays.Mods.Sections +{ + public class FunSection : ModSection + { + protected override Key[] ToggleKeys => null; + public override ModType ModType => ModType.Fun; + + public FunSection() + { + Header = @"Fun"; + } + } +} diff --git a/osu.Game/Overlays/Mods/SpecialSection.cs b/osu.Game/Overlays/Mods/SpecialSection.cs deleted file mode 100644 index b3540cf915..0000000000 --- a/osu.Game/Overlays/Mods/SpecialSection.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using OpenTK.Input; -using osu.Framework.Allocation; -using osu.Game.Graphics; -using osu.Game.Rulesets.Mods; - -namespace osu.Game.Overlays.Mods -{ - public class SpecialSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }; - public override ModType ModType => ModType.Special; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - SelectedColour = colours.BlueLight; - } - - public SpecialSection() - { - Header = @"Special"; - } - } -} diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 745f2f3def..11b68b0e09 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -195,7 +195,7 @@ namespace osu.Game.Overlays TabContainer.AutoSizeAxes |= Axes.X; TabContainer.Anchor |= Anchor.x1; TabContainer.Origin |= Anchor.x1; - Add(bottom = new Box + AddInternal(bottom = new Box { RelativeSizeAxes = Axes.X, Height = 1, diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b2f6e909d2..a3253250f2 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; @@ -23,12 +24,15 @@ namespace osu.Game.Rulesets.Edit { private readonly Ruleset ruleset; + public IEnumerable HitObjects => rulesetContainer.Playfield.AllHitObjects; + protected ICompositionTool CurrentTool { get; private set; } + protected IRulesetConfigManager Config { get; private set; } + + private readonly List layerContainers = new List(); + private readonly IBindable beatmap = new Bindable(); private RulesetContainer rulesetContainer; - private readonly List layerContainers = new List(); - - private readonly IBindable beatmap = new Bindable(); protected HitObjectComposer(Ruleset ruleset) { @@ -60,7 +64,7 @@ namespace osu.Game.Rulesets.Edit }; var layerAboveRuleset = CreateLayerContainer(); - layerAboveRuleset.Child = new HitObjectMaskLayer(rulesetContainer.Playfield, this); + layerAboveRuleset.Child = new HitObjectMaskLayer(); layerContainers.Add(layerBelowRuleset); layerContainers.Add(layerAboveRuleset); @@ -110,6 +114,16 @@ namespace osu.Game.Rulesets.Edit toolboxCollection.Items[0].Select(); } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.CacheAs(this); + Config = dependencies.Get().GetConfigFor(ruleset); + + return dependencies; + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs index 2c3720fc8f..78ad236e74 100644 --- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs @@ -8,6 +8,16 @@ namespace osu.Game.Rulesets.Edit.Tools public class HitObjectCompositionTool : ICompositionTool where T : HitObject { - public string Name => typeof(T).Name; + public string Name { get; } + + public HitObjectCompositionTool() + : this(typeof(T).Name) + { + } + + public HitObjectCompositionTool(string name) + { + Name = name; + } } } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 5de14ae579..65b2ef75c4 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Judgements private OsuColour colours; - protected readonly Judgement Judgement; + protected readonly JudgementResult Result; public readonly DrawableHitObject JudgedObject; @@ -34,11 +34,11 @@ namespace osu.Game.Rulesets.Judgements /// /// Creates a drawable which visualises a . /// - /// The judgement to visualise. + /// The judgement to visualise. /// The object which was judged. - public DrawableJudgement(Judgement judgement, DrawableHitObject judgedObject) + public DrawableJudgement(JudgementResult result, DrawableHitObject judgedObject) { - Judgement = judgement; + Result = result; JudgedObject = judgedObject; Size = new Vector2(judgement_size); @@ -49,11 +49,11 @@ namespace osu.Game.Rulesets.Judgements { this.colours = colours; - Child = new SkinnableDrawable($"Play/{Judgement.Result}", _ => JudgementText = new OsuSpriteText + Child = new SkinnableDrawable($"Play/{Result.Type}", _ => JudgementText = new OsuSpriteText { - Text = Judgement.Result.GetDescription().ToUpperInvariant(), + Text = Result.Type.GetDescription().ToUpperInvariant(), Font = @"Venera", - Colour = judgementColour(Judgement.Result), + Colour = judgementColour(Result.Type), Scale = new Vector2(0.85f, 1), TextSize = 12 }, restrictSize: false); @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Judgements this.FadeInFromZero(100, Easing.OutQuint); - switch (Judgement.Result) + switch (Result.Type) { case HitResult.None: break; diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 129dd07c3e..c679df5900 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -1,74 +1,48 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Judgements { + /// + /// The scoring information provided by a . + /// public class Judgement { - /// - /// Whether this judgement is the result of a hit or a miss. - /// - public HitResult Result; - /// /// The maximum that can be achieved. /// public virtual HitResult MaxResult => HitResult.Perfect; /// - /// The combo prior to this judgement occurring. - /// - public int ComboAtJudgement; - - /// - /// The highest combo achieved prior to this judgement occurring. - /// - public int HighestComboAtJudgement; - - /// - /// Whether a successful hit occurred. - /// - public bool IsHit => Result > HitResult.Miss; - - /// - /// Whether this judgement is the final judgement for the hit object. - /// - public bool Final = true; - - /// - /// The offset from a perfect hit at which this judgement occurred. - /// Populated when added via . - /// - public double TimeOffset { get; set; } - - /// - /// Whether the should affect the current combo. + /// Whether this should affect the current combo. /// public virtual bool AffectsCombo => true; /// - /// Whether the should be counted as base (combo) or bonus score. + /// Whether this should be counted as base (combo) or bonus score. /// public virtual bool IsBonus => !AffectsCombo; /// - /// The numeric representation for the result achieved. - /// - public int NumericResult => NumericResultFor(Result); - - /// - /// The numeric representation for the maximum achievable result. + /// The numeric score representation for the maximum achievable result. /// public int MaxNumericResult => NumericResultFor(MaxResult); /// - /// Convert a to a numeric score representation. + /// Retrieves the numeric score representation of a . /// - /// The value to convert. - /// The number. + /// The to find the numeric score representation for. + /// The numeric score representation of . protected virtual int NumericResultFor(HitResult result) => result > HitResult.Miss ? 1 : 0; + + /// + /// Retrieves the numeric score representation of a . + /// + /// The to find the numeric score representation for. + /// The numeric score representation of . + public int NumericResultFor(JudgementResult result) => NumericResultFor(result.Type); } } diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs new file mode 100644 index 0000000000..5cadf7e2ee --- /dev/null +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Judgements +{ + /// + /// The scoring result of a . + /// + public class JudgementResult + { + /// + /// Whether this is the result of a hit or a miss. + /// + public HitResult Type; + + /// + /// The which this applies for. + /// + public readonly Judgement Judgement; + + /// + /// The offset from a perfect hit at which this occurred. + /// Populated when this is applied via . + /// + public double TimeOffset { get; internal set; } + + /// + /// The combo prior to this occurring. + /// + public int ComboAtJudgement { get; internal set; } + + /// + /// The highest combo achieved prior to this occurring. + /// + public int HighestComboAtJudgement { get; internal set; } + + /// + /// Whether a miss or hit occurred. + /// + public bool HasResult => Type > HitResult.None; + + /// + /// Whether a successful hit occurred. + /// + public bool IsHit => Type > HitResult.Miss; + + /// + /// Creates a new . + /// + /// The to refer to for scoring information. + public JudgementResult(Judgement judgement) + { + Judgement = judgement; + } + } +} diff --git a/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.cs new file mode 100644 index 0000000000..be879759bd --- /dev/null +++ b/osu.Game/Rulesets/Mods/IUpdatableByPlayfield.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 + +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mods +{ + public interface IUpdatableByPlayfield : IApplicableMod + { + void Update(Playfield playfield); + } +} diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 9fb554b5c5..a991f7e7b0 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mods /// /// The type of this mod. /// - public virtual ModType Type => ModType.Special; + public virtual ModType Type => ModType.Fun; /// /// The user readable description of this mod. diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 8f73ff4c2d..5c03cb9736 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Autoplay"; public override string ShortenedName => "AT"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_auto; + public override ModType Type => ModType.Automation; public override string Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 1; public bool AllowFail => false; diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index 0bd9becd78..04aa295893 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.cs @@ -11,6 +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 ModType Type => ModType.Automation; 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 913ba23701..9c962bbcd5 100644 --- a/osu.Game/Rulesets/Mods/ModType.cs +++ b/osu.Game/Rulesets/Mods/ModType.cs @@ -7,6 +7,8 @@ namespace osu.Game.Rulesets.Mods { DifficultyReduction, DifficultyIncrease, - Special + Conversion, + Automation, + Fun } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index a22aaa784f..7e3e955740 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; +using osu.Framework.Extensions.TypeExtensions; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; @@ -35,34 +36,44 @@ namespace osu.Game.Rulesets.Objects.Drawables private readonly Lazy> nestedHitObjects = new Lazy>(); public IEnumerable NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : Enumerable.Empty(); - public event Action OnJudgement; - public event Action OnJudgementRemoved; - - public IReadOnlyList Judgements => judgements; - private readonly List judgements = new List(); + /// + /// Invoked when a has been applied by this or a nested . + /// + public event Action OnNewResult; /// - /// Whether a visible judgement should be displayed when this representation is hit. + /// Invoked when a is being reverted by this or a nested . /// - public virtual bool DisplayJudgement => true; + public event Action OnRevertResult; /// - /// Whether this and all of its nested s have been hit. + /// Whether a visual indicator should be displayed when a scoring result occurs. /// - public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && NestedHitObjects.All(n => n.IsHit); + public virtual bool DisplayResult => true; /// /// Whether this and all of its nested s have been judged. /// - public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && NestedHitObjects.All(h => h.AllJudged); + public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged); /// - /// Whether this can be judged. + /// Whether this has been hit. This occurs if is . + /// Note: This does NOT include nested hitobjects. /// - protected virtual bool ProvidesJudgement => true; + public bool IsHit => Result?.IsHit ?? false; + + /// + /// Whether this has been judged. + /// Note: This does NOT include nested hitobjects. + /// + public bool Judged => Result?.HasResult ?? true; + + /// + /// The scoring result of this . + /// + public JudgementResult Result { get; private set; } private bool judgementOccurred; - private bool judgementFinalized => judgements.LastOrDefault()?.Final == true; public bool Interactive = true; public override bool HandleKeyboardInput => Interactive; @@ -82,6 +93,14 @@ namespace osu.Game.Rulesets.Objects.Drawables [BackgroundDependencyLoader] private void load() { + var judgement = HitObject.CreateJudgement(); + if (judgement != null) + { + Result = CreateResult(judgement); + if (Result == null) + throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + } + var samples = GetSamples().ToArray(); if (samples.Any()) @@ -132,18 +151,17 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.Update(); - var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; - - while (judgements.Count > 0) + if (Result != null && Result.HasResult) { - var lastJudgement = judgements[judgements.Count - 1]; - if (lastJudgement.TimeOffset + endTime <= Time.Current) - break; + var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; - judgements.RemoveAt(judgements.Count - 1); - State.Value = ArmedState.Idle; + if (Result.TimeOffset + endTime > Time.Current) + { + OnRevertResult?.Invoke(this, Result); - OnJudgementRemoved?.Invoke(this, lastJudgement); + Result.Type = HitResult.None; + State.Value = ArmedState.Idle; + } } } @@ -151,33 +169,37 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.UpdateAfterChildren(); - UpdateJudgement(false); + UpdateResult(false); } protected virtual void AddNested(DrawableHitObject h) { - h.OnJudgement += (d, j) => OnJudgement?.Invoke(d, j); - h.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(d, j); + h.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r); + h.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r); h.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j); nestedHitObjects.Value.Add(h); } /// - /// Notifies that a new judgement has occurred for this . + /// Applies the of this , notifying responders such as + /// the of the . /// - /// The . - protected void AddJudgement(Judgement judgement) + /// The callback that applies changes to the . + protected void ApplyResult(Action application) { + application?.Invoke(Result); + + if (!Result.HasResult) + throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); + judgementOccurred = true; // Ensure that the judgement is given a valid time offset, because this may not get set by the caller var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; - judgement.TimeOffset = Time.Current - endTime; + Result.TimeOffset = Time.Current - endTime; - judgements.Add(judgement); - - switch (judgement.Result) + switch (Result.Type) { case HitResult.None: break; @@ -189,15 +211,15 @@ namespace osu.Game.Rulesets.Objects.Drawables break; } - OnJudgement?.Invoke(this, judgement); + OnNewResult?.Invoke(this, Result); } /// - /// Processes this , checking if any judgements have occurred. + /// Processes this , checking if a scoring result has occurred. /// /// Whether the user triggered this process. - /// Whether a judgement has occurred from this or any nested s. - protected bool UpdateJudgement(bool userTriggered) + /// Whether a scoring result has occurred from this or any nested . + protected bool UpdateResult(bool userTriggered) { judgementOccurred = false; @@ -205,27 +227,35 @@ namespace osu.Game.Rulesets.Objects.Drawables return false; foreach (var d in NestedHitObjects) - judgementOccurred |= d.UpdateJudgement(userTriggered); + judgementOccurred |= d.UpdateResult(userTriggered); - if (!ProvidesJudgement || judgementFinalized || judgementOccurred) + if (judgementOccurred || Judged) return judgementOccurred; var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; - CheckForJudgements(userTriggered, Time.Current - endTime); + CheckForResult(userTriggered, Time.Current - endTime); return judgementOccurred; } /// - /// Checks if any judgements have occurred for this . This method must construct - /// all s and notify of them through . + /// Checks if a scoring result has occurred for this . /// + /// + /// If a scoring result has occurred, this method must invoke to update the result and notify responders. + /// /// Whether the user triggered this check. - /// The offset from the end time at which this check occurred. A > 0 - /// implies that this check occurred after the end time of . - protected virtual void CheckForJudgements(bool userTriggered, double timeOffset) + /// The offset from the end time of the at which this check occurred. + /// A > 0 implies that this check occurred after the end time of the . + protected virtual void CheckForResult(bool userTriggered, double timeOffset) { } + + /// + /// Creates the that represents the scoring result for this . + /// + /// The that provides the scoring information. + protected virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(judgement); } public abstract class DrawableHitObject : DrawableHitObject diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 15c24e2975..beb9620f78 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -9,6 +9,7 @@ using osu.Framework.Lists; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Objects @@ -105,6 +106,12 @@ namespace osu.Game.Rulesets.Objects protected void AddNested(HitObject hitObject) => nestedHitObjects.Value.Add(hitObject); + /// + /// Creates the that represents the scoring information for this . + /// May be null. + /// + public virtual Judgement CreateJudgement() => null; + /// /// Creates the for this . /// This can be null to indicate that the has no . diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs index 50035ea116..0573a08361 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHit.cs @@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch public float X { get; set; } public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs index c7451dc978..802080aedb 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs @@ -13,21 +13,43 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { - protected override HitObject CreateHit(Vector2 position, bool newCombo) + public ConvertHitObjectParser(double offset, int formatVersion) + : base(offset, formatVersion) { + } + + private bool forceNewCombo; + private int extraComboOffset; + + protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) + { + newCombo |= forceNewCombo; + comboOffset += extraComboOffset; + + forceNewCombo = false; + extraComboOffset = 0; + return new ConvertHit { X = position.X, NewCombo = newCombo, + ComboOffset = comboOffset }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) { + newCombo |= forceNewCombo; + comboOffset += extraComboOffset; + + forceNewCombo = false; + extraComboOffset = 0; + return new ConvertSlider { X = position.X, - NewCombo = newCombo, + NewCombo = FirstObject || newCombo, + ComboOffset = comboOffset, ControlPoints = controlPoints, Distance = length, CurveType = curveType, @@ -36,15 +58,20 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch }; } - protected override HitObject CreateSpinner(Vector2 position, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) { + // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo + // Their combo offset is still added to that next hitobject's combo index + forceNewCombo |= FormatVersion <= 8 || newCombo; + extraComboOffset += comboOffset; + return new ConvertSpinner { EndTime = endTime }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs index 73e277a125..a187caaa26 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSlider.cs @@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch public float X { get; set; } public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs index 9dfe12f25e..db79ca60f1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs @@ -8,10 +8,14 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch /// /// Legacy osu!catch Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : HitObject, IHasEndTime + internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasCombo { public double EndTime { get; set; } public double Duration => EndTime - StartTime; + + public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index c48060bfa9..8236333a3f 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -19,12 +19,25 @@ namespace osu.Game.Rulesets.Objects.Legacy /// public abstract class ConvertHitObjectParser : HitObjectParser { - public override HitObject Parse(string text) + /// + /// The offset to apply to all time values. + /// + protected readonly double Offset; + + /// + /// The beatmap version. + /// + protected readonly int FormatVersion; + + protected bool FirstObject { get; private set; } = true; + + protected ConvertHitObjectParser(double offset, int formatVersion) { - return Parse(text, 0); + Offset = offset; + FormatVersion = formatVersion; } - public HitObject Parse(string text, double offset) + public override HitObject Parse(string text) { try { @@ -32,7 +45,11 @@ namespace osu.Game.Rulesets.Objects.Legacy Vector2 pos = new Vector2((int)Convert.ToSingle(split[0], CultureInfo.InvariantCulture), (int)Convert.ToSingle(split[1], CultureInfo.InvariantCulture)); - ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]) & ~ConvertHitObjectType.ColourHax; + ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]); + + int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4; + type &= ~ConvertHitObjectType.ComboOffset; + bool combo = type.HasFlag(ConvertHitObjectType.NewCombo); type &= ~ConvertHitObjectType.NewCombo; @@ -43,7 +60,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (type.HasFlag(ConvertHitObjectType.Circle)) { - result = CreateHit(pos, combo); + result = CreateHit(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); @@ -148,11 +165,11 @@ namespace osu.Game.Rulesets.Objects.Legacy for (int i = 0; i < nodes; i++) nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples); + result = CreateSlider(pos, combo, comboOffset, points, length, curveType, repeatCount, nodeSamples); } else if (type.HasFlag(ConvertHitObjectType.Spinner)) { - result = CreateSpinner(new Vector2(512, 384) / 2, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + offset); + result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + Offset); if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); @@ -170,15 +187,17 @@ namespace osu.Game.Rulesets.Objects.Legacy readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); } - result = CreateHold(pos, combo, endTime + offset); + result = CreateHold(pos, combo, comboOffset, endTime + Offset); } if (result == null) throw new InvalidOperationException($@"Unknown hit object type {type}."); - result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + offset; + result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + Offset; result.Samples = convertSoundType(soundType, bankInfo); + FirstObject = false; + return result; } catch (FormatException) @@ -221,37 +240,42 @@ namespace osu.Game.Rulesets.Objects.Legacy /// /// The position of the hit object. /// Whether the hit object creates a new combo. + /// When starting a new combo, the offset of the new combo relative to the current one. /// The hit object. - protected abstract HitObject CreateHit(Vector2 position, bool newCombo); + protected abstract HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset); /// /// Creats a legacy Slider-type hit object. /// /// The position of the hit object. /// Whether the hit object creates a new combo. + /// When starting a new combo, the offset of the new combo relative to the current one. /// The slider control points. /// The slider length. /// The slider curve type. /// The slider repeat count. /// The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider. /// The hit object. - protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples); + protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples); /// /// Creates a legacy Spinner-type hit object. /// /// The position of the hit object. + /// Whether the hit object creates a new combo. + /// When starting a new combo, the offset of the new combo relative to the current one. /// The spinner end time. /// The hit object. - protected abstract HitObject CreateSpinner(Vector2 position, double endTime); + protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime); /// /// Creates a legacy Hold-type hit object. /// /// The position of the hit object. /// Whether the hit object creates a new combo. + /// When starting a new combo, the offset of the new combo relative to the current one. /// The hold end time. - protected abstract HitObject CreateHold(Vector2 position, bool newCombo, double endTime); + protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime); private List convertSoundType(LegacySoundType type, SampleBankInfo bankInfo) { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs index c0626c3e56..fa47e56de7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Objects.Legacy Slider = 1 << 1, NewCombo = 1 << 2, Spinner = 1 << 3, - ColourHax = 112, + ComboOffset = 1 << 4 | 1 << 5 | 1 << 6, Hold = 1 << 7 } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs index ea4e7f6907..cbc8d2d4df 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs @@ -8,12 +8,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// /// Legacy osu!mania Hit-type, used for parsing Beatmaps. /// - internal sealed class ConvertHit : HitObject, IHasXPosition, IHasCombo + internal sealed class ConvertHit : HitObject, IHasXPosition { public float X { get; set; } - public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs index 99ba1304e8..6f59965e18 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs @@ -13,21 +13,24 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { - protected override HitObject CreateHit(Vector2 position, bool newCombo) + public ConvertHitObjectParser(double offset, int formatVersion) + : base(offset, formatVersion) + { + } + + protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) { return new ConvertHit { - X = position.X, - NewCombo = newCombo, + X = position.X }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) { return new ConvertSlider { X = position.X, - NewCombo = newCombo, ControlPoints = controlPoints, Distance = length, CurveType = curveType, @@ -36,7 +39,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania }; } - protected override HitObject CreateSpinner(Vector2 position, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) { return new ConvertSpinner { @@ -45,7 +48,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) { return new ConvertHold { diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs index a8d7b23df1..e1572889a3 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs @@ -8,12 +8,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania /// /// Legacy osu!mania Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition, IHasCombo + internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition { public float X { get; set; } - public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs index f015272b2c..0062e83a28 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHit.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } + public int ComboOffset { get; set; } + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index 801e4ea449..acd0de8688 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -14,21 +14,43 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { - protected override HitObject CreateHit(Vector2 position, bool newCombo) + public ConvertHitObjectParser(double offset, int formatVersion) + : base(offset, formatVersion) { + } + + private bool forceNewCombo; + private int extraComboOffset; + + protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) + { + newCombo |= forceNewCombo; + comboOffset += extraComboOffset; + + forceNewCombo = false; + extraComboOffset = 0; + return new ConvertHit { Position = position, - NewCombo = newCombo, + NewCombo = FirstObject || newCombo, + ComboOffset = comboOffset }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) { + newCombo |= forceNewCombo; + comboOffset += extraComboOffset; + + forceNewCombo = false; + extraComboOffset = 0; + return new ConvertSlider { Position = position, - NewCombo = newCombo, + NewCombo = FirstObject || newCombo, + ComboOffset = comboOffset, ControlPoints = controlPoints, Distance = Math.Max(0, length), CurveType = curveType, @@ -37,8 +59,13 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu }; } - protected override HitObject CreateSpinner(Vector2 position, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) { + // Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo + // Their combo offset is still added to that next hitobject's combo index + forceNewCombo |= FormatVersion <= 8 || newCombo; + extraComboOffset += comboOffset; + return new ConvertSpinner { Position = position, @@ -46,7 +73,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs index ec5a002bbb..45f7bc9e67 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSlider.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public bool NewCombo { get; set; } + public int ComboOffset { get; set; } + protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs index 0141785f31..3b349d9704 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu /// /// Legacy osu! Spinner-type, used for parsing Beatmaps. /// - internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasPosition + internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasPosition, IHasCombo { public double EndTime { get; set; } @@ -22,5 +22,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu public float Y => Position.Y; protected override HitWindows CreateHitWindows() => null; + + public bool NewCombo { get; set; } + + public int ComboOffset { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs index 5e9786c84a..66e504bf22 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs @@ -1,17 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Rulesets.Objects.Types; - namespace osu.Game.Rulesets.Objects.Legacy.Taiko { /// /// Legacy osu!taiko Hit-type, used for parsing Beatmaps. /// - internal sealed class ConvertHit : HitObject, IHasCombo + internal sealed class ConvertHit : HitObject { - public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs index 03b1a3187a..e5904825c2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs @@ -13,19 +13,20 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko /// public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser { - protected override HitObject CreateHit(Vector2 position, bool newCombo) + public ConvertHitObjectParser(double offset, int formatVersion) + : base(offset, formatVersion) { - return new ConvertHit - { - NewCombo = newCombo, - }; } - protected override HitObject CreateSlider(Vector2 position, bool newCombo, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) + protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset) + { + return new ConvertHit(); + } + + protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List controlPoints, double length, CurveType curveType, int repeatCount, List> repeatSamples) { return new ConvertSlider { - NewCombo = newCombo, ControlPoints = controlPoints, Distance = length, CurveType = curveType, @@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko }; } - protected override HitObject CreateSpinner(Vector2 position, double endTime) + protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime) { return new ConvertSpinner { @@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko }; } - protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) + protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime) { return null; } diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs index 8a9a0db0a7..11c0a2baae 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs @@ -1,17 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Rulesets.Objects.Types; - namespace osu.Game.Rulesets.Objects.Legacy.Taiko { /// /// Legacy osu!taiko Slider-type, used for parsing Beatmaps. /// - internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasCombo + internal sealed class ConvertSlider : Legacy.ConvertSlider { - public bool NewCombo { get; set; } - protected override HitWindows CreateHitWindows() => null; } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs index cb8b6f495a..95f1a1cb3d 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs @@ -12,5 +12,10 @@ namespace osu.Game.Rulesets.Objects.Types /// Whether the HitObject starts a new combo. /// bool NewCombo { get; } + + /// + /// When starting a new combo, the offset of the new combo relative to the current one. + /// + int ComboOffset { get; } } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 10463fd961..a6a311f6eb 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -25,5 +25,7 @@ namespace osu.Game.Rulesets public virtual Ruleset CreateInstance() => (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo), this); public bool Equals(RulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + + public override string ToString() => $"{Name} ({ShortName}) ID: {ID}"; } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9e8ea0f7c2..b0cea7009e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using osu.Framework.Configuration; +using osu.Framework.Extensions.TypeExtensions; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by the . /// - public event Action NewJudgement; + public event Action NewJudgement; /// /// Additional conditions on top of that cause a failing state. @@ -144,9 +145,10 @@ namespace osu.Game.Rulesets.Scoring /// Notifies subscribers of that a new judgement has occurred. /// /// The judgement to notify subscribers of. - protected void NotifyNewJudgement(Judgement judgement) + /// The judgement scoring result to notify subscribers of. + protected void NotifyNewJudgement(JudgementResult result) { - NewJudgement?.Invoke(judgement); + NewJudgement?.Invoke(result); if (HasCompleted) AllJudged?.Invoke(); @@ -194,9 +196,10 @@ namespace osu.Game.Rulesets.Scoring { Debug.Assert(base_portion + combo_portion == 1.0); - rulesetContainer.OnJudgement += AddJudgement; - rulesetContainer.OnJudgementRemoved += RemoveJudgement; + rulesetContainer.OnNewResult += applyResult; + rulesetContainer.OnRevertResult += revertResult; + ApplyBeatmap(rulesetContainer.Beatmap); SimulateAutoplay(rulesetContainer.Beatmap); Reset(true); @@ -210,46 +213,80 @@ namespace osu.Game.Rulesets.Scoring } /// - /// 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. - /// + /// Applies any properties of the which affect scoring to this . /// - /// The containing the s that will be judged by this . - protected virtual void SimulateAutoplay(Beatmap beatmap) { } + /// The to read properties from. + protected virtual void ApplyBeatmap(Beatmap beatmap) + { + } /// - /// Adds a judgement to this ScoreProcessor. + /// Simulates an autoplay of the to determine scoring values. /// - /// The judgement to add. - protected void AddJudgement(Judgement judgement) + /// This provided temporarily. DO NOT USE. + /// The to simulate. + protected virtual void SimulateAutoplay(Beatmap beatmap) { - OnNewJudgement(judgement); + foreach (var obj in beatmap.HitObjects) + simulate(obj); + + void simulate(HitObject obj) + { + foreach (var nested in obj.NestedHitObjects) + simulate(nested); + + var judgement = obj.CreateJudgement(); + if (judgement == null) + return; + + var result = CreateResult(judgement); + if (result == null) + throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + + result.Type = judgement.MaxResult; + + applyResult(result); + } + } + + /// + /// Applies the score change of a to this . + /// + /// The to apply. + private void applyResult(JudgementResult result) + { + ApplyResult(result); updateScore(); UpdateFailed(); - NotifyNewJudgement(judgement); + NotifyNewJudgement(result); } - protected void RemoveJudgement(Judgement judgement) + /// + /// Reverts the score change of a that was applied to this . + /// + /// The judgement to remove. + /// The judgement scoring result. + private void revertResult(JudgementResult result) { - OnJudgementRemoved(judgement); + RevertResult(result); updateScore(); } /// - /// Applies a judgement. + /// Applies the score change of a to this . /// - /// The judgement to apply/ - protected virtual void OnNewJudgement(Judgement judgement) + /// The to apply. + protected virtual void ApplyResult(JudgementResult result) { - judgement.ComboAtJudgement = Combo; - judgement.HighestComboAtJudgement = HighestCombo; + result.ComboAtJudgement = Combo; + result.HighestComboAtJudgement = HighestCombo; - if (judgement.AffectsCombo) + JudgedHits++; + + if (result.Judgement.AffectsCombo) { - switch (judgement.Result) + switch (result.Type) { case HitResult.None: break; @@ -260,43 +297,41 @@ namespace osu.Game.Rulesets.Scoring Combo.Value++; break; } - - JudgedHits++; } - if (judgement.IsBonus) + if (result.Judgement.IsBonus) { - if (judgement.IsHit) - bonusScore += judgement.NumericResult; + if (result.IsHit) + bonusScore += result.Judgement.NumericResultFor(result); } else { - baseScore += judgement.NumericResult; - rollingMaxBaseScore += judgement.MaxNumericResult; + baseScore += result.Judgement.NumericResultFor(result); + rollingMaxBaseScore += result.Judgement.MaxNumericResult; } } /// - /// Removes a judgement. This should reverse everything in . + /// Reverts the score change of a that was applied to this . /// /// The judgement to remove. - protected virtual void OnJudgementRemoved(Judgement judgement) + /// The judgement scoring result. + protected virtual void RevertResult(JudgementResult result) { - Combo.Value = judgement.ComboAtJudgement; - HighestCombo.Value = judgement.HighestComboAtJudgement; + Combo.Value = result.ComboAtJudgement; + HighestCombo.Value = result.HighestComboAtJudgement; - if (judgement.AffectsCombo) - JudgedHits--; + JudgedHits--; - if (judgement.IsBonus) + if (result.Judgement.IsBonus) { - if (judgement.IsHit) - bonusScore -= judgement.NumericResult; + if (result.IsHit) + bonusScore -= result.Judgement.NumericResultFor(result); } else { - baseScore -= judgement.NumericResult; - rollingMaxBaseScore -= judgement.MaxNumericResult; + baseScore -= result.Judgement.NumericResultFor(result); + rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } } @@ -333,6 +368,12 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore = 0; bonusScore = 0; } + + /// + /// Creates the that represents the scoring result for a . + /// + /// The that provides the scoring information. + protected virtual JudgementResult CreateResult(Judgement judgement) => new JudgementResult(judgement); } public enum ScoringMode diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index cdc1248049..92e9a4831f 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -79,10 +79,18 @@ namespace osu.Game.Rulesets.UI backgroundColour = colours.Green; highlightedColour = colours.GreenLight; break; - case ModType.Special: + case ModType.Automation: backgroundColour = colours.Blue; highlightedColour = colours.BlueLight; break; + case ModType.Conversion: + backgroundColour = colours.Purple; + highlightedColour = colours.PurpleLight; + break; + case ModType.Fun: + backgroundColour = colours.Pink; + highlightedColour = colours.PinkLight; + break; } applyStyle(); @@ -92,10 +100,7 @@ namespace osu.Game.Rulesets.UI public bool Highlighted { - get - { - return highlighted; - } + get { return highlighted; } set { diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 2f44d99e18..da14fb54d6 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -1,26 +1,37 @@ // 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.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Configuration; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.UI { public abstract class Playfield : ScalableContainer { /// - /// The HitObjects contained in this Playfield. + /// The contained in this Playfield. /// public HitObjectContainer HitObjects { get; private set; } /// - /// All the s nested inside this playfield. + /// All the s contained in this and all . /// - public IReadOnlyList NestedPlayfields => nestedPlayfields; - private List nestedPlayfields; + public IEnumerable AllHitObjects => HitObjects?.Objects.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)) ?? Enumerable.Empty(); + + /// + /// All s nested inside this . + /// + public IEnumerable NestedPlayfields => nestedPlayfields.IsValueCreated ? nestedPlayfields.Value : Enumerable.Empty(); + + private readonly Lazy> nestedPlayfields = new Lazy>(); /// /// Whether judgements should be displayed by this and and all nested s. @@ -42,9 +53,13 @@ namespace osu.Game.Rulesets.UI RelativeSizeAxes = Axes.Both; } + private WorkingBeatmap beatmap; + [BackgroundDependencyLoader] - private void load() + private void load(IBindableBeatmap beatmap) { + this.beatmap = beatmap.Value; + HitObjects = CreateHitObjectContainer(); HitObjects.RelativeSizeAxes = Axes.Both; @@ -54,7 +69,7 @@ namespace osu.Game.Rulesets.UI /// /// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield. /// - public virtual void PostProcess() => nestedPlayfields?.ForEach(p => p.PostProcess()); + public virtual void PostProcess() => NestedPlayfields.ForEach(p => p.PostProcess()); /// /// Adds a DrawableHitObject to this Playfield. @@ -75,17 +90,23 @@ namespace osu.Game.Rulesets.UI /// The to add. protected void AddNested(Playfield otherPlayfield) { - if (nestedPlayfields == null) - nestedPlayfields = new List(); - - nestedPlayfields.Add(otherPlayfield); - otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); + nestedPlayfields.Value.Add(otherPlayfield); } /// /// Creates the container that will be used to contain the s. /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); + + protected override void Update() + { + base.Update(); + + if (beatmap != null) + foreach (var mod in beatmap.Mods.Value) + if (mod is IUpdatableByPlayfield updatable) + updatable.Update(this); + } } } diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index ee34e2df04..64ee680d45 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -182,8 +182,15 @@ namespace osu.Game.Rulesets.UI public abstract class RulesetContainer : RulesetContainer where TObject : HitObject { - public event Action OnJudgement; - public event Action OnJudgementRemoved; + /// + /// Invoked when a has been applied by a . + /// + public event Action OnNewResult; + + /// + /// Invoked when a is being reverted by a . + /// + public event Action OnRevertResult; /// /// The Beatmap @@ -290,8 +297,8 @@ namespace osu.Game.Rulesets.UI if (drawableObject == null) continue; - drawableObject.OnJudgement += (d, j) => OnJudgement?.Invoke(j); - drawableObject.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(j); + drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r); + drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r); Playfield.Add(drawableObject); } diff --git a/osu.Game/Screens/Edit/Menus/ScreenSelectionTabControl.cs b/osu.Game/Screens/Edit/Menus/ScreenSelectionTabControl.cs index 1471a37a29..f58e5b39eb 100644 --- a/osu.Game/Screens/Edit/Menus/ScreenSelectionTabControl.cs +++ b/osu.Game/Screens/Edit/Menus/ScreenSelectionTabControl.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.Menus TabContainer.AutoSizeAxes = Axes.X; TabContainer.Padding = new MarginPadding(); - Add(new Box + AddInternal(new Box { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs index 03ac8e91f0..d212bbe7dd 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.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.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -9,30 +8,24 @@ using osu.Framework.Input.EventArgs; using osu.Framework.Input.States; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.UI; namespace osu.Game.Screens.Edit.Screens.Compose.Layers { public class HitObjectMaskLayer : CompositeDrawable { - private readonly Playfield playfield; - private readonly HitObjectComposer composer; - private MaskContainer maskContainer; + private HitObjectComposer composer; - public HitObjectMaskLayer(Playfield playfield, HitObjectComposer composer) + public HitObjectMaskLayer() { - // we need the playfield as HitObjects may not be initialised until its BDL. - this.playfield = playfield; - - this.composer = composer; - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load() + private void load(HitObjectComposer composer) { + this.composer = composer; + maskContainer = new MaskContainer(); var maskSelection = composer.CreateMaskSelection(); @@ -55,7 +48,7 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers dragLayer.CreateProxy() }; - foreach (var obj in playfield.HitObjects.Objects) + foreach (var obj in composer.HitObjects) addMask(obj); } @@ -77,18 +70,5 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers maskContainer.Add(mask); } - - /// - /// Removes the mask for a . - /// - /// The to remove the mask for. - private void removeMask(DrawableHitObject hitObject) - { - var mask = maskContainer.FirstOrDefault(h => h.HitObject == hitObject); - if (mask == null) - return; - - maskContainer.Remove(mask); - } } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index ce00686c02..7f2bc1d357 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -278,7 +278,7 @@ namespace osu.Game.Screens.Menu if (logo != null) { - if (logoTracking && iconFacade.IsLoaded) + if (logoTracking && logo.RelativePositionAxes == Axes.None && iconFacade.IsLoaded) logo.Position = logoTrackingPosition; iconFacade.Width = logo.SizeForFlow * 0.5f; diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 894322dd41..1a164b473d 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -63,6 +63,12 @@ namespace osu.Game.Screens.Play.HUD }; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + mods.UnbindAll(); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs index ab446550a6..b551b1f7a6 100644 --- a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs @@ -92,9 +92,9 @@ namespace osu.Game.Screens.Play.HUD }; } - public void Flash(Judgement judgement) + public void Flash(JudgementResult result) { - if (judgement.Result == HitResult.Miss) + if (result.Type == HitResult.Miss) return; fill.FadeEdgeEffectTo(Math.Min(1, fill.EdgeEffect.Colour.Linear.A + (1f - base_glow_opacity) / glow_max_hits), 50, Easing.OutQuint) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 00ba1a8d12..2e23bb16f0 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -213,7 +213,7 @@ namespace osu.Game.Screens.Play { if (!IsCurrentScreen) return; - pauseContainer.Hide(); + fadeOut(true); Restart(); }, } @@ -278,6 +278,8 @@ namespace osu.Game.Screens.Play ScoreProcessor.PopulateScore(score); score.User = RulesetContainer.Replay?.User ?? api.LocalUser.Value; Push(new Results(score)); + + onCompletionEvent = null; }); } } @@ -340,6 +342,13 @@ namespace osu.Game.Screens.Play protected override bool OnExiting(Screen next) { + if (onCompletionEvent != null) + { + // Proceed to result screen if beatmap already finished playing + onCompletionEvent.RunTask(); + return true; + } + if ((!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) && (!pauseContainer?.IsResuming ?? true)) { // In the case of replays, we may have changed the playback rate. @@ -355,16 +364,10 @@ namespace osu.Game.Screens.Play return true; } - private void fadeOut() + private void fadeOut(bool instant = false) { - const float fade_out_duration = 250; - - RulesetContainer?.FadeOut(fade_out_duration); - Content.FadeOut(fade_out_duration); - - hudOverlay?.ScaleTo(0.7f, fade_out_duration * 3, Easing.In); - - Background?.FadeTo(1f, fade_out_duration); + float fadeOutDuration = instant ? 0 : 250; + Content.FadeOut(fadeOutDuration); } protected override bool OnScroll(InputState state) => mouseWheelDisabled.Value && !pauseContainer.IsPaused; diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 4abc0dfcd9..fd4322c268 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -1,12 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.EventArgs; using osu.Framework.Input.States; using osu.Framework.Localisation; using osu.Framework.Screens; @@ -14,14 +14,18 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Screens.Play { public class PlayerLoader : ScreenWithBeatmapBackground { + private static readonly Vector2 background_blur = new Vector2(15); + private Player player; private BeatmapMetadataDisplay info; @@ -62,26 +66,30 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding(25), Children = new PlayerSettingsGroup[] { - new VisualSettings(), + visualSettings = new VisualSettings(), new InputSettings() } }); - loadTask = LoadComponentAsync(player); + loadTask = LoadComponentAsync(player, playerLoaded); } + private void playerLoaded(Player player) => info.Loading = false; + protected override void OnResuming(Screen last) { base.OnResuming(last); contentIn(); + info.Loading = true; + //we will only be resumed if the player has requested a re-run (see ValidForResume setting above) loadTask = LoadComponentAsync(player = new Player { RestartCount = player.RestartCount + 1, RestartRequested = player.RestartRequested, - }); + }, playerLoaded); this.Delay(400).Schedule(pushWhenLoaded); } @@ -123,23 +131,33 @@ namespace osu.Game.Screens.Play logo.Delay(resuming ? 0 : 500).MoveToOffset(new Vector2(0, -0.24f), 500, Easing.InOutExpo); } - private bool weHandledMouseDown; - - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) - { - weHandledMouseDown = true; - return base.OnMouseDown(state, args); - } - - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) - { - weHandledMouseDown = false; - return base.OnMouseUp(state, args); - } - private ScheduledDelegate pushDebounce; + private VisualSettings visualSettings; - private bool readyForPush => player.LoadState == LoadState.Ready && IsHovered && (!GetContainingInputManager().CurrentState.Mouse.HasAnyButtonPressed || weHandledMouseDown); + private bool readyForPush => player.LoadState == LoadState.Ready && IsHovered && GetContainingInputManager()?.DraggedDrawable == null; + + protected override bool OnHover(InputState state) + { + // restore our screen defaults + InitializeBackgroundElements(); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + if (GetContainingInputManager().HoveredDrawables.Contains(visualSettings)) + { + // show user setting preview + UpdateBackgroundElements(); + } + base.OnHoverLost(state); + } + + protected override void InitializeBackgroundElements() + { + Background?.FadeTo(1, BACKGROUND_FADE_DURATION, Easing.OutQuint); + Background?.BlurTo(background_blur, BACKGROUND_FADE_DURATION, Easing.OutQuint); + } private void pushWhenLoaded() { @@ -231,7 +249,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.TopCentre, Origin = Anchor.TopRight, Margin = new MarginPadding { Right = 5 }, - Colour = OsuColour.Gray(0.5f), + Colour = OsuColour.Gray(0.8f), Text = left, }, new OsuSpriteText @@ -246,6 +264,25 @@ namespace osu.Game.Screens.Play } private readonly WorkingBeatmap beatmap; + private LoadingAnimation loading; + private Sprite backgroundSprite; + + public bool Loading + { + set + { + if (value) + { + loading.Show(); + backgroundSprite.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); + } + else + { + loading.Hide(); + backgroundSprite.FadeColour(Color4.White, 400, Easing.OutQuint); + } + } + } public BeatmapMetadataDisplay(WorkingBeatmap beatmap) { @@ -292,9 +329,9 @@ namespace osu.Game.Screens.Play Anchor = Anchor.TopCentre, CornerRadius = 10, Masking = true, - Children = new[] + Children = new Drawable[] { - new Sprite + backgroundSprite = new Sprite { RelativeSizeAxes = Axes.Both, Texture = beatmap?.Background, @@ -302,6 +339,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, FillMode = FillMode.Fill, }, + loading = new LoadingAnimation { Scale = new Vector2(1.3f) } } }, new OsuSpriteText @@ -329,6 +367,8 @@ namespace osu.Game.Screens.Play }, } }; + + Loading = true; } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs index c4767f404d..64c49099f2 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs @@ -128,6 +128,27 @@ namespace osu.Game.Screens.Play.PlayerSettings }; } + private const float fade_duration = 800; + private const float inactive_alpha = 0.5f; + + protected override void LoadComplete() + { + base.LoadComplete(); + this.Delay(600).FadeTo(inactive_alpha, fade_duration, Easing.OutQuint); + } + + protected override bool OnHover(InputState state) + { + this.FadeIn(fade_duration, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(InputState state) + { + this.FadeTo(inactive_alpha, fade_duration, Easing.OutQuint); + base.OnHoverLost(state); + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -140,7 +161,6 @@ namespace osu.Game.Screens.Play.PlayerSettings protected override Container Content => content; - protected override bool OnHover(InputState state) => true; protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; } } diff --git a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs index 7f18305b1c..26d3218fbf 100644 --- a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs +++ b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs @@ -15,6 +15,8 @@ namespace osu.Game.Screens.Play { protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); + protected new BackgroundScreenBeatmap Background => (BackgroundScreenBeatmap)base.Background; + public override bool AllowBeatmapRulesetChange => false; protected const float BACKGROUND_FADE_DURATION = 800; @@ -43,21 +45,30 @@ namespace osu.Game.Screens.Play DimLevel.ValueChanged += _ => UpdateBackgroundElements(); BlurLevel.ValueChanged += _ => UpdateBackgroundElements(); ShowStoryboard.ValueChanged += _ => UpdateBackgroundElements(); - UpdateBackgroundElements(); + InitializeBackgroundElements(); } protected override void OnResuming(Screen last) { base.OnResuming(last); - UpdateBackgroundElements(); + InitializeBackgroundElements(); } + /// + /// Called once on entering screen. By Default, performs a full call. + /// + protected virtual void InitializeBackgroundElements() => UpdateBackgroundElements(); + + /// + /// Called wen background elements require updates, usually due to a user changing a setting. + /// + /// protected virtual void UpdateBackgroundElements() { if (!IsCurrentScreen) return; Background?.FadeTo(BackgroundOpacity, BACKGROUND_FADE_DURATION, Easing.OutQuint); - (Background as BackgroundScreenBeatmap)?.BlurTo(new Vector2((float)BlurLevel.Value * 25), BACKGROUND_FADE_DURATION, Easing.OutQuint); + Background?.BlurTo(new Vector2((float)BlurLevel.Value * 25), BACKGROUND_FADE_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 3e946288d9..06837c9274 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -4,6 +4,8 @@ using System; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; @@ -214,17 +216,21 @@ namespace osu.Game.Screens.Play private Box background; private AspectContainer aspect; + private SampleChannel sampleConfirm; + public Button() { RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { colourNormal = colours.Yellow; colourHover = colours.YellowDark; + sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); + Children = new Drawable[] { background = new Box @@ -311,6 +317,8 @@ namespace osu.Game.Screens.Play if (!Enabled) return false; + sampleConfirm.Play(); + box.FlashColour(Color4.White, 500, Easing.OutQuint); aspect.ScaleTo(1.2f, 2000, Easing.OutQuint); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f1721d8941..6e4454a311 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -330,13 +330,13 @@ namespace osu.Game.Screens.Select private FilterCriteria activeCriteria = new FilterCriteria(); - protected ScheduledDelegate FilterTask; + protected ScheduledDelegate PendingFilter; public bool AllowSelection = true; public void FlushPendingFilterOperations() { - if (FilterTask?.Completed == false) + if (PendingFilter?.Completed == false) { applyActiveCriteria(false, false); Update(); @@ -357,18 +357,18 @@ namespace osu.Game.Screens.Select void perform() { - FilterTask = null; + PendingFilter = null; root.Filter(activeCriteria); itemsCache.Invalidate(); if (scroll) scrollPositionCache.Invalidate(); } - FilterTask?.Cancel(); - FilterTask = null; + PendingFilter?.Cancel(); + PendingFilter = null; if (debounce) - FilterTask = Scheduler.AddDelayed(perform, 250); + PendingFilter = Scheduler.AddDelayed(perform, 250); else perform(); } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 274b859e82..9ba8b085f3 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.Select showConverted.ValueChanged += val => updateCriteria(); ruleset.BindTo(parentRuleset); - ruleset.BindValueChanged(val => updateCriteria(), true); + ruleset.BindValueChanged(_ => updateCriteria(), true); } private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria()); diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index a346911ca2..e914eb365e 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -50,13 +50,14 @@ namespace osu.Game.Screens.Select private SampleChannel sampleConfirm; - public readonly Bindable> SelectedMods = new Bindable>(new List()); + [Cached] + [Cached(Type = typeof(IBindable>))] + private readonly Bindable> selectedMods = new Bindable>(new Mod[] { }); [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, OsuGame osu) + private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, Bindable> selectedMods) { - if (osu != null) SelectedMods.BindTo(osu.SelectedMods); - modSelect.SelectedMods.BindTo(SelectedMods); + if (selectedMods != null) this.selectedMods.BindTo(selectedMods); sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); @@ -84,7 +85,7 @@ namespace osu.Game.Screens.Select protected override void UpdateBeatmap(WorkingBeatmap beatmap) { - beatmap.Mods.BindTo(SelectedMods); + beatmap.Mods.BindTo(selectedMods); base.UpdateBeatmap(beatmap); @@ -131,7 +132,7 @@ namespace osu.Game.Screens.Select if (Beatmap.Value.Track != null) Beatmap.Value.Track.Looping = false; - SelectedMods.UnbindAll(); + selectedMods.UnbindAll(); Beatmap.Value.Mods.Value = new Mod[] { }; return false; @@ -147,10 +148,10 @@ namespace osu.Game.Screens.Select var auto = Ruleset.Value.CreateInstance().GetAutoplayMod(); var autoType = auto.GetType(); - var mods = modSelect.SelectedMods.Value; + var mods = selectedMods.Value; if (mods.All(m => m.GetType() != autoType)) { - modSelect.SelectedMods.Value = mods.Append(auto); + selectedMods.Value = mods.Append(auto); removeAutoModOnResume = true; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 3cf406fabb..dcc0760262 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -12,6 +12,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Input.EventArgs; using osu.Framework.Input.States; using osu.Framework.Screens; @@ -199,10 +200,6 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader(true)] private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuColour colours) { - // manual binding to parent ruleset to allow for delayed load in the incoming direction. - base.Ruleset.ValueChanged += r => updateSelectedBeatmap(beatmapNoDebounce); - Ruleset.ValueChanged += r => base.Ruleset.Value = r; - if (Footer != null) { Footer.AddButton(@"random", colours.Green, triggerRandom, Key.F2); @@ -225,15 +222,6 @@ namespace osu.Game.Screens.Select sampleChangeBeatmap = audio.Sample.Get(@"SongSelect/select-expand"); Carousel.BeatmapSets = this.beatmaps.GetAllUsableBeatmapSetsEnumerable(); - - Beatmap.BindDisabledChanged(disabled => Carousel.AllowSelection = !disabled, true); - Beatmap.BindValueChanged(workingBeatmapChanged); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - base.Ruleset.ValueChanged += r => updateSelectedBeatmap(beatmapNoDebounce); } public void Edit(BeatmapInfo beatmap) @@ -296,59 +284,83 @@ namespace osu.Game.Screens.Select private BeatmapInfo beatmapNoDebounce; private RulesetInfo rulesetNoDebounce; + private void updateSelectedBeatmap(BeatmapInfo beatmap) + { + if (beatmap?.Equals(beatmapNoDebounce) == true) + return; + + beatmapNoDebounce = beatmap; + performUpdateSelected(); + } + + private void updateSelectedRuleset(RulesetInfo ruleset) + { + if (ruleset?.Equals(rulesetNoDebounce) == true) + return; + + rulesetNoDebounce = ruleset; + performUpdateSelected(); + } + /// /// selection has been changed as the result of a user interaction. /// - private void updateSelectedBeatmap(BeatmapInfo beatmap) + private void performUpdateSelected() { - var ruleset = base.Ruleset.Value; + var beatmap = beatmapNoDebounce; + var ruleset = rulesetNoDebounce; - void performLoad() + void run() { - WorkingBeatmap working = Beatmap.Value; + Logger.Log($"updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ID.ToString() ?? "null"}"); + bool preview = false; + if (ruleset?.Equals(Ruleset.Value) == false) + { + Logger.Log($"ruleset changed from \"{Ruleset.Value}\" to \"{ruleset}\""); + + Beatmap.Value.Mods.Value = Enumerable.Empty(); + Ruleset.Value = ruleset; + + // force a filter before attempting to change the beatmap. + // we may still be in the wrong ruleset as there is a debounce delay on ruleset changes. + Carousel.Filter(null, false); + + // Filtering only completes after the carousel runs Update. + // If we also have a pending beatmap change we should delay it one frame. + selectionChangedDebounce = Schedule(run); + return; + } + // We may be arriving here due to another component changing the bindable Beatmap. // In these cases, the other component has already loaded the beatmap, so we don't need to do so again. - if (beatmap?.Equals(Beatmap.Value.BeatmapInfo) != true) + if (!Equals(beatmap, Beatmap.Value.BeatmapInfo)) { + Logger.Log($"beatmap changed from \"{Beatmap.Value.BeatmapInfo}\" to \"{beatmap}\""); + preview = beatmap?.BeatmapSetInfoID != Beatmap.Value?.BeatmapInfo.BeatmapSetInfoID; - working = beatmaps.GetWorkingBeatmap(beatmap, Beatmap.Value); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap.Value); + + if (beatmap != null) + { + if (beatmap.BeatmapSetInfoID == beatmapNoDebounce?.BeatmapSetInfoID) + sampleChangeDifficulty.Play(); + else + sampleChangeBeatmap.Play(); + } } - - working.Mods.Value = Enumerable.Empty(); - - Beatmap.Value = working; - Ruleset.Value = ruleset; - ensurePlayingSelected(preview); - UpdateBeatmap(Beatmap.Value); } - if (beatmap?.Equals(beatmapNoDebounce) == true && ruleset?.Equals(rulesetNoDebounce) == true) - return; - selectionChangedDebounce?.Cancel(); - beatmapNoDebounce = beatmap; - rulesetNoDebounce = ruleset; - if (beatmap == null) - performLoad(); + run(); else - { - if (beatmap.BeatmapSetInfoID == beatmapNoDebounce?.BeatmapSetInfoID) - sampleChangeDifficulty.Play(); - else - sampleChangeBeatmap.Play(); - - if (beatmap == Beatmap.Value.BeatmapInfo) - performLoad(); - else - selectionChangedDebounce = Scheduler.AddDelayed(performLoad, 200); - } + selectionChangedDebounce = Scheduler.AddDelayed(run, 200); } private void triggerRandom() @@ -448,6 +460,8 @@ namespace osu.Game.Screens.Select { base.Dispose(isDisposing); + Ruleset.UnbindAll(); + if (beatmaps != null) { beatmaps.ItemAdded -= onBeatmapSetAdded; @@ -464,6 +478,8 @@ namespace osu.Game.Screens.Select /// The working beatmap. protected virtual void UpdateBeatmap(WorkingBeatmap beatmap) { + Logger.Log($"working beatmap updated to {beatmap}"); + if (Background is BackgroundScreenBeatmap backgroundModeBeatmap) { backgroundModeBeatmap.Beatmap = beatmap; @@ -495,6 +511,17 @@ namespace osu.Game.Screens.Select private void carouselBeatmapsLoaded() { + if (rulesetNoDebounce == null) + { + // manual binding to parent ruleset to allow for delayed load in the incoming direction. + rulesetNoDebounce = Ruleset.Value = base.Ruleset.Value; + base.Ruleset.ValueChanged += updateSelectedRuleset; + Ruleset.ValueChanged += r => base.Ruleset.Value = r; + + Beatmap.BindDisabledChanged(disabled => Carousel.AllowSelection = !disabled, true); + Beatmap.BindValueChanged(workingBeatmapChanged); + } + if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) return; @@ -503,7 +530,7 @@ namespace osu.Game.Screens.Select { // in the case random selection failed, we want to trigger selectionChanged // to show the dummy beatmap (we have nothing else to display). - updateSelectedBeatmap(null); + performUpdateSelected(); } } diff --git a/osu.Game/Utils/RavenLogger.cs b/osu.Game/Utils/RavenLogger.cs new file mode 100644 index 0000000000..b28dd1fb73 --- /dev/null +++ b/osu.Game/Utils/RavenLogger.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 System.Collections.Generic; +using System.Threading.Tasks; +using osu.Framework.Logging; +using SharpRaven; +using SharpRaven.Data; + +namespace osu.Game.Utils +{ + /// + /// Report errors to sentry. + /// + public class RavenLogger : IDisposable + { + private readonly RavenClient raven = new RavenClient("https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255"); + + private readonly List tasks = new List(); + + private Exception lastException; + + public RavenLogger(OsuGame game) + { + raven.Release = game.Version; + + if (!game.IsDeployedBuild) return; + + Logger.NewEntry += entry => + { + if (entry.Level < LogLevel.Verbose) return; + + var exception = entry.Exception; + + if (exception != null) + { + // since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports. + if (lastException != null && + lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace)) + { + return; + } + + lastException = exception; + queuePendingTask(raven.CaptureAsync(new SentryEvent(exception))); + } + else + raven.AddTrail(new Breadcrumb(entry.Target.ToString(), BreadcrumbType.Navigation) { Message = entry.Message }); + }; + } + + private void queuePendingTask(Task task) + { + lock (tasks) tasks.Add(task); + task.ContinueWith(_ => + { + lock (tasks) + tasks.Remove(task); + }); + } + + #region Disposal + + ~RavenLogger() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool isDisposed; + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposed) + return; + + isDisposed = true; + lock (tasks) Task.WaitAll(tasks.ToArray(), 5000); + } + + #endregion + } +} diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 56a1979382..06fb1c4f82 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,9 +18,10 @@ - - + + + \ No newline at end of file diff --git a/osu.Game/osu.nuspec b/osu.Game/osu.nuspec deleted file mode 100644 index bb7d382cee..0000000000 --- a/osu.Game/osu.nuspec +++ /dev/null @@ -1,26 +0,0 @@ - - - - osulazer - 0.0.0 - osulazer - ppy Pty Ltd - Dean Herbert - https://osu.ppy.sh/ - https://puu.sh/tYyXZ/9a01a5d1b0.ico - false - click the circles. to the beat. - click the circles. - testing - Copyright ppy Pty Ltd 2007-2018 - en-AU - - - - - - - - - - diff --git a/osu.TestProject.props b/osu.TestProject.props index c2e6048a60..a73a4f8ce2 100644 --- a/osu.TestProject.props +++ b/osu.TestProject.props @@ -13,9 +13,6 @@ - - -