mirror of
https://github.com/ppy/osu.git
synced 2026-05-13 20:33:35 +08:00
Compare commits
1735 Commits
@@ -36,7 +36,7 @@ jobs:
|
||||
generator:
|
||||
name: Run
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 720
|
||||
timeout-minutes: 1440
|
||||
|
||||
outputs:
|
||||
target: ${{ steps.run.outputs.target }}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
name: Pack and nuget
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
notify_pending_production_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Submit pending deployment notification
|
||||
run: |
|
||||
export TITLE="Pending osu Production Deployment: $GITHUB_REF_NAME"
|
||||
export URL="https://github.com/ppy/osu/actions/runs/$GITHUB_RUN_ID"
|
||||
export DESCRIPTION="Awaiting approval for building NuGet packages for tag $GITHUB_REF_NAME:
|
||||
[View Workflow Run]($URL)"
|
||||
export ACTOR_ICON="https://avatars.githubusercontent.com/u/$GITHUB_ACTOR_ID"
|
||||
|
||||
BODY="$(jq --null-input '{
|
||||
"embeds": [
|
||||
{
|
||||
"title": env.TITLE,
|
||||
"color": 15098112,
|
||||
"description": env.DESCRIPTION,
|
||||
"url": env.URL,
|
||||
"author": {
|
||||
"name": env.GITHUB_ACTOR,
|
||||
"icon_url": env.ACTOR_ICON
|
||||
}
|
||||
}
|
||||
]
|
||||
}')"
|
||||
|
||||
curl \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY" \
|
||||
"${{ secrets.DISCORD_INFRA_WEBHOOK_URL }}"
|
||||
|
||||
pack:
|
||||
name: Pack
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set artifacts directory
|
||||
id: artifactsPath
|
||||
run: echo "::set-output name=nuget_artifacts::${{github.workspace}}/artifacts"
|
||||
|
||||
- name: Install .NET 8.0.x
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Pack
|
||||
run: |
|
||||
# Replace project references in templates with package reference, because they're included as source files.
|
||||
dotnet remove Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game/osu.Game.csproj
|
||||
dotnet remove Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj reference osu.Game/osu.Game.csproj
|
||||
dotnet remove Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game/osu.Game.csproj
|
||||
dotnet remove Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj reference osu.Game/osu.Game.csproj
|
||||
|
||||
dotnet add Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform/osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
|
||||
dotnet add Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
|
||||
dotnet add Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling/osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
|
||||
dotnet add Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -n -v ${{ github.ref_name }}
|
||||
|
||||
# Pack
|
||||
dotnet pack -c Release osu.Game /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||
dotnet pack -c Release osu.Game.Rulesets.Osu /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||
dotnet pack -c Release osu.Game.Rulesets.Taiko /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||
dotnet pack -c Release osu.Game.Rulesets.Catch /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||
dotnet pack -c Release osu.Game.Rulesets.Mania /p:Version=${{ github.ref_name }} /p:GenerateDocumentationFile=true /p:IncludeSymbols=true /p:SymbolPackageFormat=snupkg -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||
dotnet pack -c Release Templates /p:Version=${{ github.ref_name }} -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: osu
|
||||
path: |
|
||||
${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg
|
||||
${{steps.artifactsPath.outputs.nuget_artifacts}}/*.snupkg
|
||||
|
||||
- name: Publish packages to nuget.org
|
||||
run: dotnet nuget push ${{steps.artifactsPath.outputs.nuget_artifacts}}/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
|
||||
@@ -21,3 +21,7 @@ M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberiz
|
||||
M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead.
|
||||
M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
||||
M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
||||
M:TagLib.File.Create(System.String);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||
M:TagLib.File.Create(TagLib.File.IFileAbstraction);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||
M:TagLib.File.Create(System.String,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||
M:TagLib.File.Create(TagLib.File.IFileAbstraction,TagLib.ReadStyle);TagLib's MIME type detection changes behaviour depending on CultureInfo.CurrentCulture. Use TagLibUtils.GetTagLibFile() instead.
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
<PropertyGroup Label="C#">
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Stabilises hot reload, see: https://platform.uno/docs/articles/studio/Hot%20Reload/hot-reload-overview.html?tabs=vswin%2Cwindows%2Cskia-desktop%2Ccommon-issues -->
|
||||
<GenerateAssemblyInfo Condition="'$(Configuration)'=='Debug'">false</GenerateAssemblyInfo>
|
||||
<!-- Required due to the above -->
|
||||
<NoWarn Condition="'$(Configuration)'=='Debug'">$(NoWarn);CA1416</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>$(MSBuildThisFileDirectory)app.manifest</ApplicationManifest>
|
||||
@@ -46,7 +50,7 @@
|
||||
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
|
||||
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
|
||||
<Company>ppy Pty Ltd</Company>
|
||||
<Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright>
|
||||
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
|
||||
<PackageTags>osu game</PackageTags>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2024 ppy Pty Ltd <contact@ppy.sh>.
|
||||
Copyright (c) 2025 ppy Pty Ltd <contact@ppy.sh>.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+4
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osuTK;
|
||||
|
||||
@@ -17,5 +18,8 @@ namespace osu.Game.Rulesets.EmptyFreeform.Replays
|
||||
if (button.HasValue)
|
||||
Actions.Add(button.Value);
|
||||
}
|
||||
|
||||
public override bool IsEquivalentTo(ReplayFrame other)
|
||||
=> other is EmptyFreeformReplayFrame freeformFrame && Time == freeformFrame.Time && Position == freeformFrame.Position && Actions.SequenceEqual(freeformFrame.Actions);
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -9,5 +9,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
|
||||
public class PippidonReplayFrame : ReplayFrame
|
||||
{
|
||||
public Vector2 Position;
|
||||
|
||||
public override bool IsEquivalentTo(ReplayFrame other)
|
||||
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Position == pippidonFrame.Position;
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.EmptyScrolling.Replays
|
||||
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.EmptyScrolling.Replays
|
||||
if (button.HasValue)
|
||||
Actions.Add(button.Value);
|
||||
}
|
||||
|
||||
public override bool IsEquivalentTo(ReplayFrame other)
|
||||
=> other is EmptyScrollingReplayFrame scrollingFrame && Time == scrollingFrame.Time && Actions.SequenceEqual(scrollingFrame.Actions);
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.Pippidon.Replays
|
||||
@@ -15,5 +16,8 @@ namespace osu.Game.Rulesets.Pippidon.Replays
|
||||
if (button.HasValue)
|
||||
Actions.Add(button.Value);
|
||||
}
|
||||
|
||||
public override bool IsEquivalentTo(ReplayFrame other)
|
||||
=> other is PippidonReplayFrame pippidonFrame && Time == pippidonFrame.Time && Actions.SequenceEqual(pippidonFrame.Actions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
|
||||
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
|
||||
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
|
||||
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
|
||||
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
|
||||
<PackageTags>dotnet-new;templates;osu</PackageTags>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
clone_depth: 1
|
||||
version: '{branch}-{build}'
|
||||
image: Visual Studio 2022
|
||||
cache:
|
||||
- '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
|
||||
|
||||
dotnet_csproj:
|
||||
patch: true
|
||||
file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
|
||||
version: '0.0.{build}'
|
||||
|
||||
before_build:
|
||||
- cmd: dotnet --info # Useful when version mismatch between CI and local
|
||||
- cmd: dotnet workload install maui-android # Change to `dotnet workload restore` once there's no old projects
|
||||
- cmd: dotnet workload install maui-ios # Change to `dotnet workload restore` once there's no old projects
|
||||
- cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
|
||||
|
||||
build:
|
||||
project: osu.sln
|
||||
parallel: true
|
||||
verbosity: minimal
|
||||
publish_nuget: true
|
||||
|
||||
after_build:
|
||||
- ps: .\InspectCode.ps1
|
||||
|
||||
test:
|
||||
assemblies:
|
||||
except:
|
||||
- '**\*Android*'
|
||||
- '**\*iOS*'
|
||||
- 'build\**\*'
|
||||
@@ -1,86 +0,0 @@
|
||||
clone_depth: 1
|
||||
version: '{build}'
|
||||
image: Visual Studio 2022
|
||||
test: off
|
||||
skip_non_tags: true
|
||||
configuration: Release
|
||||
|
||||
environment:
|
||||
matrix:
|
||||
- job_name: osu-game
|
||||
- job_name: osu-ruleset
|
||||
job_depends_on: osu-game
|
||||
- job_name: taiko-ruleset
|
||||
job_depends_on: osu-game
|
||||
- job_name: catch-ruleset
|
||||
job_depends_on: osu-game
|
||||
- job_name: mania-ruleset
|
||||
job_depends_on: osu-game
|
||||
- job_name: templates
|
||||
job_depends_on: osu-game
|
||||
|
||||
nuget:
|
||||
project_feed: true
|
||||
|
||||
for:
|
||||
-
|
||||
matrix:
|
||||
only:
|
||||
- job_name: osu-game
|
||||
build_script:
|
||||
- cmd: dotnet pack osu.Game\osu.Game.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
||||
-
|
||||
matrix:
|
||||
only:
|
||||
- job_name: osu-ruleset
|
||||
build_script:
|
||||
- cmd: dotnet remove osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj reference osu.Game\osu.Game.csproj
|
||||
- cmd: dotnet add osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
||||
- cmd: dotnet pack osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
||||
-
|
||||
matrix:
|
||||
only:
|
||||
- job_name: taiko-ruleset
|
||||
build_script:
|
||||
- cmd: dotnet remove osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj reference osu.Game\osu.Game.csproj
|
||||
- cmd: dotnet add osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
||||
- cmd: dotnet pack osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
||||
-
|
||||
matrix:
|
||||
only:
|
||||
- job_name: catch-ruleset
|
||||
build_script:
|
||||
- cmd: dotnet remove osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj reference osu.Game\osu.Game.csproj
|
||||
- cmd: dotnet add osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
||||
- cmd: dotnet pack osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
||||
-
|
||||
matrix:
|
||||
only:
|
||||
- job_name: mania-ruleset
|
||||
build_script:
|
||||
- cmd: dotnet remove osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj reference osu.Game\osu.Game.csproj
|
||||
- cmd: dotnet add osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
||||
- cmd: dotnet pack osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
||||
-
|
||||
matrix:
|
||||
only:
|
||||
- job_name: templates
|
||||
build_script:
|
||||
- cmd: dotnet remove Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj reference osu.Game\osu.Game.csproj
|
||||
- cmd: dotnet remove Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
|
||||
- cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj reference osu.Game\osu.Game.csproj
|
||||
- cmd: dotnet remove Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj reference osu.Game\osu.Game.csproj
|
||||
|
||||
- cmd: dotnet add Templates\Rulesets\ruleset-empty\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
||||
- cmd: dotnet add Templates\Rulesets\ruleset-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
||||
- cmd: dotnet add Templates\Rulesets\ruleset-scrolling-empty\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
||||
- cmd: dotnet add Templates\Rulesets\ruleset-scrolling-example\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME%
|
||||
|
||||
- cmd: dotnet pack Templates\osu.Game.Templates.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME%
|
||||
|
||||
artifacts:
|
||||
- path: '**\*.nupkg'
|
||||
|
||||
deploy:
|
||||
- provider: Environment
|
||||
name: nuget
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.321.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.710.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Framework.Logging;
|
||||
using osu.Game.Updater;
|
||||
using osu.Desktop.Windows;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
@@ -33,6 +34,8 @@ namespace osu.Desktop
|
||||
[Cached(typeof(IHighPerformanceSessionManager))]
|
||||
private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
|
||||
|
||||
public bool IsFirstRun { get; init; }
|
||||
|
||||
public OsuGameDesktop(string[]? args = null)
|
||||
: base(args)
|
||||
{
|
||||
@@ -104,6 +107,14 @@ namespace osu.Desktop
|
||||
|
||||
protected override UpdateManager CreateUpdateManager()
|
||||
{
|
||||
// If this is the first time we've run the game, ie it is being installed,
|
||||
// reset the user's release stream to "lazer".
|
||||
//
|
||||
// This ensures that if a user is trying to recover from a failed startup on an unstable release stream,
|
||||
// the game doesn't immediately try and update them back to the release stream after starting up.
|
||||
if (IsFirstRun)
|
||||
LocalConfig.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
|
||||
|
||||
if (IsPackageManaged)
|
||||
return new NoActionUpdateManager();
|
||||
|
||||
|
||||
+13
-4
@@ -28,6 +28,8 @@ namespace osu.Desktop
|
||||
|
||||
private static LegacyTcpIpcProvider? legacyIpc;
|
||||
|
||||
private static bool isFirstRun;
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
@@ -135,7 +137,12 @@ namespace osu.Desktop
|
||||
if (tournamentClient)
|
||||
host.Run(new TournamentGame());
|
||||
else
|
||||
host.Run(new OsuGameDesktop(args));
|
||||
{
|
||||
host.Run(new OsuGameDesktop(args)
|
||||
{
|
||||
IsFirstRun = isFirstRun
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +184,8 @@ namespace osu.Desktop
|
||||
|
||||
var app = VelopackApp.Build();
|
||||
|
||||
app.OnFirstRun(_ => isFirstRun = true);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
configureWindows(app);
|
||||
|
||||
@@ -186,9 +195,9 @@ namespace osu.Desktop
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void configureWindows(VelopackApp app)
|
||||
{
|
||||
app.WithFirstRun(_ => WindowsAssociationManager.InstallAssociations());
|
||||
app.WithAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
|
||||
app.WithBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
|
||||
app.OnFirstRun(_ => WindowsAssociationManager.InstallAssociations());
|
||||
app.OnAfterUpdateFastCallback(_ => WindowsAssociationManager.UpdateAssociations());
|
||||
app.OnBeforeUninstallFastCallback(_ => WindowsAssociationManager.UninstallAssociations());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,25 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens.Play;
|
||||
using Velopack;
|
||||
using Velopack.Sources;
|
||||
using UpdateManager = osu.Game.Updater.UpdateManager;
|
||||
|
||||
namespace osu.Desktop.Updater
|
||||
{
|
||||
public partial class VelopackUpdateManager : Game.Updater.UpdateManager
|
||||
public partial class VelopackUpdateManager : UpdateManager
|
||||
{
|
||||
private readonly UpdateManager updateManager;
|
||||
private INotificationOverlay notificationOverlay = null!;
|
||||
[Resolved]
|
||||
private INotificationOverlay notificationOverlay { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
@@ -27,122 +30,117 @@ namespace osu.Desktop.Updater
|
||||
|
||||
private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying;
|
||||
|
||||
private UpdateInfo? pendingUpdate;
|
||||
private ScheduledDelegate? scheduledBackgroundCheck;
|
||||
|
||||
public VelopackUpdateManager()
|
||||
private void scheduleNextUpdateCheck()
|
||||
{
|
||||
updateManager = new UpdateManager(new GithubSource(@"https://github.com/ppy/osu", null, false), new UpdateOptions
|
||||
scheduledBackgroundCheck?.Cancel();
|
||||
scheduledBackgroundCheck = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
AllowVersionDowngrade = true,
|
||||
log("Running scheduled background update check...");
|
||||
CheckForUpdate();
|
||||
}, 60000 * 30);
|
||||
}
|
||||
|
||||
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
|
||||
{
|
||||
scheduledBackgroundCheck?.Cancel();
|
||||
|
||||
if (isInGameplay)
|
||||
{
|
||||
log("Update check cancelled - user is in gameplay");
|
||||
scheduleNextUpdateCheck();
|
||||
return false;
|
||||
}
|
||||
|
||||
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
|
||||
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
|
||||
{
|
||||
AllowVersionDowngrade = true
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(INotificationOverlay notifications)
|
||||
{
|
||||
notificationOverlay = notifications;
|
||||
}
|
||||
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
||||
|
||||
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
||||
|
||||
private async Task<bool> checkForUpdateAsync()
|
||||
{
|
||||
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
|
||||
bool scheduleRecheck = false;
|
||||
|
||||
try
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Avoid any kind of update checking while gameplay is running.
|
||||
if (isInGameplay)
|
||||
{
|
||||
scheduleRecheck = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
|
||||
// Velopack does support this scenario (see https://github.com/ppy/osu/pull/28743#discussion_r1743495975).
|
||||
if (pendingUpdate != null)
|
||||
{
|
||||
// If there is an update pending restart, show the notification to restart again.
|
||||
notificationOverlay.Post(new UpdateApplicationCompleteNotification
|
||||
{
|
||||
Activated = () =>
|
||||
{
|
||||
Task.Run(restartToApplyUpdate);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pendingUpdate = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
||||
|
||||
// No update is available. We'll check again later.
|
||||
if (pendingUpdate == null)
|
||||
{
|
||||
scheduleRecheck = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// An update is found, let's notify the user and start downloading it.
|
||||
UpdateProgressNotification notification = new UpdateProgressNotification
|
||||
{
|
||||
CompletionClickAction = () =>
|
||||
{
|
||||
Task.Run(restartToApplyUpdate);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
runOutsideOfGameplay(() => notificationOverlay.Post(notification));
|
||||
notification.StartDownload();
|
||||
|
||||
try
|
||||
{
|
||||
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
runOutsideOfGameplay(() => notification.State = ProgressNotificationState.Completed);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// In the case of an error, a separate notification will be displayed.
|
||||
scheduleRecheck = true;
|
||||
notification.FailDownload();
|
||||
Logger.Error(e, @"update failed!");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
|
||||
scheduleRecheck = true;
|
||||
Logger.Log($@"update check failed ({e.Message})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (scheduleRecheck)
|
||||
{
|
||||
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
|
||||
}
|
||||
log("Update check cancelled");
|
||||
scheduleNextUpdateCheck();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (update == null)
|
||||
{
|
||||
// No update is available.
|
||||
log("No update found");
|
||||
scheduleNextUpdateCheck();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Download update in the background while notifying awaiters of the update being available.
|
||||
log($"New update available: {update.TargetFullRelease.Version}");
|
||||
downloadUpdate(updateManager, update, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void runOutsideOfGameplay(Action action)
|
||||
private void downloadUpdate(Velopack.UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () =>
|
||||
{
|
||||
log($"Beginning download of update {update.TargetFullRelease.Version}...");
|
||||
|
||||
UpdateDownloadProgressNotification progressNotification = new UpdateDownloadProgressNotification(cancellationToken)
|
||||
{
|
||||
CompletionClickAction = () =>
|
||||
{
|
||||
restartToApplyUpdate(updateManager, update);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(progressNotification.CancellationToken, cancellationToken))
|
||||
{
|
||||
progressNotification.StartDownload();
|
||||
runOutsideOfGameplay(() => notificationOverlay.Post(progressNotification), cts.Token);
|
||||
|
||||
await updateManager.DownloadUpdatesAsync(update, p => progressNotification.Progress = p / 100f, cts.Token).ConfigureAwait(false);
|
||||
runOutsideOfGameplay(() => progressNotification.State = ProgressNotificationState.Completed, cts.Token);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
progressNotification.FailDownload();
|
||||
log(@"Update cancelled");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// In the case of an error, a separate notification will be displayed.
|
||||
progressNotification.FailDownload();
|
||||
Logger.Error(e, @"Update failed!");
|
||||
}
|
||||
|
||||
return true;
|
||||
}, cancellationToken);
|
||||
|
||||
private void runOutsideOfGameplay(Action action, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (isInGameplay)
|
||||
{
|
||||
Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000);
|
||||
Scheduler.AddDelayed(() => runOutsideOfGameplay(action, cancellationToken), 1000);
|
||||
return;
|
||||
}
|
||||
|
||||
action();
|
||||
}
|
||||
|
||||
private async Task restartToApplyUpdate()
|
||||
private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update) => Task.Run(async () =>
|
||||
{
|
||||
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);
|
||||
await updateManager.WaitExitThenApplyUpdatesAsync(update.TargetFullRelease).ConfigureAwait(false);
|
||||
Schedule(() => game.AttemptExit());
|
||||
}
|
||||
});
|
||||
|
||||
private static void log(string text) => Logger.Log($"VelopackUpdateManager: {text}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="9.0.2" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Velopack" Version="0.0.1053" />
|
||||
<PackageReference Include="Velopack" Version="0.0.1298" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
<EmbeddedResource Include="lazer.ico" />
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
|
||||
<releaseNotes>testing</releaseNotes>
|
||||
<copyright>Copyright (c) 2024 ppy Pty Ltd</copyright>
|
||||
<copyright>Copyright (c) 2025 ppy Pty Ltd</copyright>
|
||||
<language>en-AU</language>
|
||||
</metadata>
|
||||
<files>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Fruit { StartTime = 0, },
|
||||
new Fruit { StartTime = 5000, },
|
||||
new Fruit { StartTime = 10000, },
|
||||
new Fruit { StartTime = 15000, }
|
||||
}
|
||||
};
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
|
||||
[Test]
|
||||
public void TestRecording()
|
||||
{
|
||||
seekTo(0);
|
||||
AddStep("start moving left", () => InputManager.PressKey(Key.Left));
|
||||
seekTo(5000);
|
||||
AddStep("end moving left", () => InputManager.ReleaseKey(Key.Left));
|
||||
AddAssert("catcher max left", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(0));
|
||||
AddAssert("movement to left recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.MoveLeft])));
|
||||
AddAssert("replay reached left edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, 0)));
|
||||
|
||||
AddStep("start dashing right", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.PressKey(Key.Right);
|
||||
});
|
||||
seekTo(10000);
|
||||
AddStep("end dashing right", () =>
|
||||
{
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
InputManager.ReleaseKey(Key.Right);
|
||||
});
|
||||
AddAssert("catcher max right", () => this.ChildrenOfType<Catcher>().Single().X, () => Is.EqualTo(CatchPlayfield.WIDTH));
|
||||
AddAssert("dash to right recorded to replay", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => f.Actions.SequenceEqual([CatchAction.Dash, CatchAction.MoveRight])));
|
||||
AddAssert("replay reached right edge", () => Player.Score.Replay.Frames.OfType<CatchReplayFrame>().Any(f => Precision.AlmostEquals(f.Position, CatchPlayfield.WIDTH)));
|
||||
}
|
||||
|
||||
private void seekTo(double time)
|
||||
{
|
||||
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
@@ -16,26 +18,30 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
int fruits = HitObjects.Count(s => s is Fruit);
|
||||
int juiceStreams = HitObjects.Count(s => s is JuiceStream);
|
||||
int bananaShowers = HitObjects.Count(s => s is BananaShower);
|
||||
int sum = Math.Max(1, fruits + juiceStreams);
|
||||
|
||||
return new[]
|
||||
{
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Fruit Count",
|
||||
Name = BeatmapStatisticStrings.Fruits,
|
||||
Content = fruits.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
|
||||
BarDisplayLength = fruits / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Juice Stream Count",
|
||||
Name = BeatmapStatisticStrings.JuiceStreams,
|
||||
Content = juiceStreams.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
|
||||
BarDisplayLength = juiceStreams / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Banana Shower Count",
|
||||
Name = BeatmapStatisticStrings.BananaShowers,
|
||||
Content = bananaShowers.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
|
||||
BarDisplayLength = Math.Min(bananaShowers / 10f, 1),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Catch.Edit.Checks;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
@@ -13,7 +14,11 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
// Compose
|
||||
new CheckBananaShowerGap(),
|
||||
new CheckConcurrentObjects(),
|
||||
|
||||
// Settings
|
||||
new CheckCatchAbnormalDifficultySettings(),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -219,5 +220,40 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
|
||||
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
|
||||
}
|
||||
|
||||
#region Clipboard handling
|
||||
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
|
||||
|
||||
// 1,2,3,4 ...
|
||||
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
|
||||
|
||||
public override void SelectFromTimestamp(double timestamp, string objectDescription)
|
||||
{
|
||||
if (!selection_regex.IsMatch(objectDescription))
|
||||
return;
|
||||
|
||||
List<CatchHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<CatchHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
||||
string[] splitDescription = objectDescription.Split(',');
|
||||
|
||||
for (int i = 0; i < splitDescription.Length; i++)
|
||||
{
|
||||
if (!int.TryParse(splitDescription[i], out int combo) || combo < 1)
|
||||
continue;
|
||||
|
||||
CatchHitObject? current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo);
|
||||
|
||||
if (current == null)
|
||||
continue;
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.Add(current);
|
||||
|
||||
if (i < splitDescription.Length - 1)
|
||||
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
@@ -89,6 +90,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
KeyboardStep = 1,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
|
||||
public BindableBool HardRockOffsets { get; } = new BindableBool();
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate))
|
||||
return string.Empty;
|
||||
|
||||
if (!CircleSize.IsDefault) return format("CS", CircleSize);
|
||||
if (!ApproachRate.IsDefault) return format("AR", ApproachRate);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -2,12 +2,20 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!";
|
||||
|
||||
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
base.ApplyToDifficulty(difficulty);
|
||||
|
||||
difficulty.OverallDifficulty *= ADJUST_RATIO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
base.ApplyToDifficulty(difficulty);
|
||||
|
||||
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
|
||||
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
|
||||
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
@@ -64,5 +65,12 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
|
||||
return new LegacyReplayFrame(Time, Position, null, state);
|
||||
}
|
||||
|
||||
public override bool IsEquivalentTo(ReplayFrame other)
|
||||
=> other is CatchReplayFrame catchFrame
|
||||
&& Time == catchFrame.Time
|
||||
&& Position == catchFrame.Position
|
||||
&& Dashing == catchFrame.Dashing
|
||||
&& Actions.SequenceEqual(catchFrame.Actions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||
|
||||
if (keyCounter != null)
|
||||
{
|
||||
@@ -64,12 +65,20 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
spectatorList.Origin = Anchor.BottomLeft;
|
||||
spectatorList.Position = new Vector2(10, -10);
|
||||
}
|
||||
|
||||
if (leaderboard != null)
|
||||
{
|
||||
leaderboard.Anchor = Anchor.CentreLeft;
|
||||
leaderboard.Origin = Anchor.CentreLeft;
|
||||
leaderboard.X = 10;
|
||||
}
|
||||
})
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new LegacyKeyCounterDisplay(),
|
||||
new SpectatorList(),
|
||||
new DrawableGameplayLeaderboard(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -68,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
// needs to be scaled down to remain playable.
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float aspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
scaleContainer.Scale = new Vector2(base_aspect_ratio / aspectRatio);
|
||||
scaleContainer.Scale = new Vector2(Math.Min(1, base_aspect_ratio / aspectRatio));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Mania.Edit.Checks;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckManiaConcurrentObjectsTest
|
||||
{
|
||||
private CheckConcurrentObjects check = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckManiaConcurrentObjects();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHoldNotesSeparateOnSameColumn()
|
||||
{
|
||||
assertOk(new List<HitObject>
|
||||
{
|
||||
createHoldNote(startTime: 100, endTime: 400.75d, column: 1),
|
||||
createHoldNote(startTime: 500, endTime: 900.75d, column: 1)
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHoldNotesConcurrentOnDifferentColumns()
|
||||
{
|
||||
assertOk(new List<HitObject>
|
||||
{
|
||||
createHoldNote(startTime: 100, endTime: 400.75d, column: 1),
|
||||
createHoldNote(startTime: 300, endTime: 700.75d, column: 2)
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHoldNotesConcurrentOnSameColumn()
|
||||
{
|
||||
assertConcurrentSame(new List<HitObject>
|
||||
{
|
||||
createHoldNote(startTime: 100, endTime: 400.75d, column: 1),
|
||||
createHoldNote(startTime: 300, endTime: 700.75d, column: 1)
|
||||
});
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitobjects)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitobjects)), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertConcurrentSame(List<HitObject> hitobjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitobjects)
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject> { HitObjects = hitobjects };
|
||||
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
}
|
||||
|
||||
private HoldNote createHoldNote(double startTime, double endTime, int column)
|
||||
{
|
||||
return new HoldNote
|
||||
{
|
||||
StartTime = startTime,
|
||||
EndTime = endTime,
|
||||
Column = column
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManiaFilterCriteriaTest
|
||||
{
|
||||
[TestCase]
|
||||
public void TestKeysEqualSingleValue()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1");
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysEqualMultipleValues()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1,3,5,7");
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysNotEqualSingleValue()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysNotEqualMultipleValues()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1,3,5,7");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey1()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestKeysGreaterOrEqualThan()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria
|
||||
{
|
||||
Mods = [new ManiaModKey7()]
|
||||
}));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestFilterIntersection()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
|
||||
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "7");
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.False(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 7 }),
|
||||
new FilterCriteria()));
|
||||
|
||||
Assert.True(criteria.Matches(
|
||||
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 9 }),
|
||||
new FilterCriteria()));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestInvalidFilters()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
@@ -38,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
|
||||
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
|
||||
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } },
|
||||
new object[] { LegacyMods.ScoreV2, new[] { typeof(ManiaModScoreV2) } },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(mania_mod_mapping))]
|
||||
|
||||
@@ -31,9 +31,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
public void TestHoldNote(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new HoldNote { StartTime = 1000, EndTime = 3000 }), shouldMiss);
|
||||
|
||||
[Test]
|
||||
public void TestGreatHit() => CreateModTest(new ModTestData
|
||||
public void TestPerfectHits([Values] bool requirePerfectHits) => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModPerfect(),
|
||||
Mod = new ManiaModPerfect
|
||||
{
|
||||
RequirePerfectHits = { Value = requirePerfectHits }
|
||||
},
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
@@ -47,6 +50,32 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
}
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestGreatHit([Values] bool requirePerfectHits) => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModPerfect
|
||||
{
|
||||
RequirePerfectHits = { Value = requirePerfectHits }
|
||||
},
|
||||
PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(requirePerfectHits),
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = 1000,
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1020, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(2000)
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
foreach (var holdNote in CreatedDrawables.SelectMany(d => d.ChildrenOfType<DrawableHoldNote>()))
|
||||
{
|
||||
((Bindable<bool>)holdNote.IsHitting).Value = v;
|
||||
((Bindable<bool>)holdNote.IsHolding).Value = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,768 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
|
||||
{
|
||||
protected override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
|
||||
protected override string? ExportLocation => null;
|
||||
|
||||
private static readonly object[][] score_v2_test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
|
||||
// Note that mania hitwindows are heavily idiosyncratic,
|
||||
// and if you *think* a number here is wrong, probably double check.
|
||||
|
||||
// Known issues / complexities:
|
||||
// - There is a disparate set of hitwindow ranges for: score V1 non-converts, score V1 converts, and score V2 (regardless of convert)
|
||||
// - It is NEVER POSSIBLE to get a MEH result when late; exceeding the OK hit windows will result in a MISS.
|
||||
// Additionally, the OK hit window when late is EXCLUSIVE / OPEN rather than INCLUSIVE / CLOSED.
|
||||
// Relevant stable source: https://github.com/peppy/osu-stable-reference/blob/996648fba06baf4e7d2e0b248959399444017895/osu!/GameplayElements/HitObjectManagerMania.cs#L737-L751
|
||||
// - There is also a seemingly mania-specific issue wherein key inputs registered before time instant 0 get truncated to time 0,
|
||||
// which is why the beatmaps used below make sure not to cross that boundary (the note starts at t=300ms).
|
||||
// This is not an issue in osu! or taiko.
|
||||
// The source of this behaviour has not been investigated in detail.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// PERFECT hit window is [ -19ms, 19ms]
|
||||
// GREAT hit window is [ -49ms, 49ms]
|
||||
// GOOD hit window is [ -82ms, 82ms]
|
||||
// OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -18d, HitResult.Perfect },
|
||||
new object[] { 5f, -19d, HitResult.Perfect },
|
||||
new object[] { 5f, -20d, HitResult.Great },
|
||||
new object[] { 5f, -21d, HitResult.Great },
|
||||
new object[] { 5f, -48d, HitResult.Great },
|
||||
new object[] { 5f, -49d, HitResult.Great },
|
||||
new object[] { 5f, -50d, HitResult.Good },
|
||||
new object[] { 5f, -51d, HitResult.Good },
|
||||
new object[] { 5f, -81d, HitResult.Good },
|
||||
new object[] { 5f, -82d, HitResult.Good },
|
||||
new object[] { 5f, -83d, HitResult.Ok },
|
||||
new object[] { 5f, -84d, HitResult.Ok },
|
||||
new object[] { 5f, -111d, HitResult.Ok },
|
||||
new object[] { 5f, -112d, HitResult.Ok },
|
||||
new object[] { 5f, -113d, HitResult.Meh },
|
||||
new object[] { 5f, -114d, HitResult.Meh },
|
||||
new object[] { 5f, -135d, HitResult.Meh },
|
||||
new object[] { 5f, -136d, HitResult.Meh },
|
||||
new object[] { 5f, -137d, HitResult.Miss },
|
||||
new object[] { 5f, -138d, HitResult.Miss },
|
||||
new object[] { 5f, 111d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 5f, 112d, HitResult.Miss },
|
||||
// new object[] { 5f, 113d, HitResult.Miss },
|
||||
// new object[] { 5f, 114d, HitResult.Miss },
|
||||
// new object[] { 5f, 135d, HitResult.Miss },
|
||||
// new object[] { 5f, 136d, HitResult.Miss },
|
||||
// new object[] { 5f, 137d, HitResult.Miss },
|
||||
// new object[] { 5f, 138d, HitResult.Miss },
|
||||
|
||||
// OD = 9.3 test cases.
|
||||
// PERFECT hit window is [ -14ms, 14ms]
|
||||
// GREAT hit window is [ -36ms, 36ms]
|
||||
// GOOD hit window is [ -69ms, 69ms]
|
||||
// OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 9.3f, 13d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 14d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 15d, HitResult.Great },
|
||||
new object[] { 9.3f, 16d, HitResult.Great },
|
||||
new object[] { 9.3f, 35d, HitResult.Great },
|
||||
new object[] { 9.3f, 36d, HitResult.Great },
|
||||
new object[] { 9.3f, 37d, HitResult.Good },
|
||||
new object[] { 9.3f, 38d, HitResult.Good },
|
||||
new object[] { 9.3f, 68d, HitResult.Good },
|
||||
new object[] { 9.3f, 69d, HitResult.Good },
|
||||
new object[] { 9.3f, 70d, HitResult.Ok },
|
||||
new object[] { 9.3f, 71d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 9.3f, 99d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 100d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 101d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 122d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 123d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 124d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 125d, HitResult.Miss },
|
||||
new object[] { 9.3f, -98d, HitResult.Ok },
|
||||
new object[] { 9.3f, -99d, HitResult.Ok },
|
||||
new object[] { 9.3f, -100d, HitResult.Meh },
|
||||
new object[] { 9.3f, -101d, HitResult.Meh },
|
||||
new object[] { 9.3f, -122d, HitResult.Meh },
|
||||
new object[] { 9.3f, -123d, HitResult.Meh },
|
||||
new object[] { 9.3f, -124d, HitResult.Miss },
|
||||
new object[] { 9.3f, -125d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] score_v1_non_convert_test_cases =
|
||||
{
|
||||
// OD = 5 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -49ms, 49ms]
|
||||
// GOOD hit window is [ -82ms, 82ms]
|
||||
// OK hit window is [-112ms, 112ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-136ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -15d, HitResult.Perfect },
|
||||
new object[] { 5f, -16d, HitResult.Perfect },
|
||||
new object[] { 5f, -17d, HitResult.Great },
|
||||
new object[] { 5f, -18d, HitResult.Great },
|
||||
new object[] { 5f, -48d, HitResult.Great },
|
||||
new object[] { 5f, -49d, HitResult.Great },
|
||||
new object[] { 5f, -50d, HitResult.Good },
|
||||
new object[] { 5f, -51d, HitResult.Good },
|
||||
new object[] { 5f, -81d, HitResult.Good },
|
||||
new object[] { 5f, -82d, HitResult.Good },
|
||||
new object[] { 5f, -83d, HitResult.Ok },
|
||||
new object[] { 5f, -84d, HitResult.Ok },
|
||||
new object[] { 5f, -111d, HitResult.Ok },
|
||||
new object[] { 5f, -112d, HitResult.Ok },
|
||||
new object[] { 5f, -113d, HitResult.Meh },
|
||||
new object[] { 5f, -114d, HitResult.Meh },
|
||||
new object[] { 5f, -135d, HitResult.Meh },
|
||||
new object[] { 5f, -136d, HitResult.Meh },
|
||||
new object[] { 5f, -137d, HitResult.Miss },
|
||||
new object[] { 5f, -138d, HitResult.Miss },
|
||||
new object[] { 5f, 111d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 5f, 112d, HitResult.Miss },
|
||||
// new object[] { 5f, 113d, HitResult.Miss },
|
||||
// new object[] { 5f, 114d, HitResult.Miss },
|
||||
// new object[] { 5f, 135d, HitResult.Miss },
|
||||
// new object[] { 5f, 136d, HitResult.Miss },
|
||||
// new object[] { 5f, 137d, HitResult.Miss },
|
||||
// new object[] { 5f, 138d, HitResult.Miss },
|
||||
|
||||
// OD = 9.3 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -36ms, 36ms]
|
||||
// GOOD hit window is [ -69ms, 69ms]
|
||||
// OK hit window is [ -99ms, 99ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-123ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 9.3f, 15d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 16d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 17d, HitResult.Great },
|
||||
new object[] { 9.3f, 18d, HitResult.Great },
|
||||
new object[] { 9.3f, 35d, HitResult.Great },
|
||||
new object[] { 9.3f, 36d, HitResult.Great },
|
||||
new object[] { 9.3f, 37d, HitResult.Good },
|
||||
new object[] { 9.3f, 38d, HitResult.Good },
|
||||
new object[] { 9.3f, 68d, HitResult.Good },
|
||||
new object[] { 9.3f, 69d, HitResult.Good },
|
||||
new object[] { 9.3f, 70d, HitResult.Ok },
|
||||
new object[] { 9.3f, 71d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 9.3f, 99d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 100d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 101d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 122d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 123d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 124d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 125d, HitResult.Miss },
|
||||
new object[] { 9.3f, -98d, HitResult.Ok },
|
||||
new object[] { 9.3f, -99d, HitResult.Ok },
|
||||
new object[] { 9.3f, -100d, HitResult.Meh },
|
||||
new object[] { 9.3f, -101d, HitResult.Meh },
|
||||
new object[] { 9.3f, -122d, HitResult.Meh },
|
||||
new object[] { 9.3f, -123d, HitResult.Meh },
|
||||
new object[] { 9.3f, -124d, HitResult.Miss },
|
||||
new object[] { 9.3f, -125d, HitResult.Miss },
|
||||
|
||||
// OD = 3.1 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -54ms, 54ms]
|
||||
// GOOD hit window is [ -87ms, 87ms]
|
||||
// OK hit window is [-117ms, 117ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-141ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 3.1f, 15d, HitResult.Perfect },
|
||||
new object[] { 3.1f, 16d, HitResult.Perfect },
|
||||
new object[] { 3.1f, 17d, HitResult.Great },
|
||||
new object[] { 3.1f, 18d, HitResult.Great },
|
||||
new object[] { 3.1f, 53d, HitResult.Great },
|
||||
new object[] { 3.1f, 54d, HitResult.Great },
|
||||
new object[] { 3.1f, 55d, HitResult.Good },
|
||||
new object[] { 3.1f, 56d, HitResult.Good },
|
||||
new object[] { 3.1f, 86d, HitResult.Good },
|
||||
new object[] { 3.1f, 87d, HitResult.Good },
|
||||
new object[] { 3.1f, 88d, HitResult.Ok },
|
||||
new object[] { 3.1f, 89d, HitResult.Ok },
|
||||
new object[] { 3.1f, 116d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 3.1f, 117d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 118d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 119d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 140d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 141d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 142d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 143d, HitResult.Miss },
|
||||
new object[] { 3.1f, -116d, HitResult.Ok },
|
||||
new object[] { 3.1f, -117d, HitResult.Ok },
|
||||
new object[] { 3.1f, -118d, HitResult.Meh },
|
||||
new object[] { 3.1f, -119d, HitResult.Meh },
|
||||
new object[] { 3.1f, -140d, HitResult.Meh },
|
||||
new object[] { 3.1f, -141d, HitResult.Meh },
|
||||
new object[] { 3.1f, -142d, HitResult.Miss },
|
||||
new object[] { 3.1f, -143d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] score_v1_convert_test_cases =
|
||||
{
|
||||
// OD = 5 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -34ms, 34ms]
|
||||
// GOOD hit window is [ -67ms, 67ms]
|
||||
// OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -15d, HitResult.Perfect },
|
||||
new object[] { 5f, -16d, HitResult.Perfect },
|
||||
new object[] { 5f, -17d, HitResult.Great },
|
||||
new object[] { 5f, -18d, HitResult.Great },
|
||||
new object[] { 5f, -33d, HitResult.Great },
|
||||
new object[] { 5f, -34d, HitResult.Great },
|
||||
new object[] { 5f, -35d, HitResult.Good },
|
||||
new object[] { 5f, -36d, HitResult.Good },
|
||||
new object[] { 5f, -66d, HitResult.Good },
|
||||
new object[] { 5f, -67d, HitResult.Good },
|
||||
new object[] { 5f, -68d, HitResult.Ok },
|
||||
new object[] { 5f, -69d, HitResult.Ok },
|
||||
new object[] { 5f, -96d, HitResult.Ok },
|
||||
new object[] { 5f, -97d, HitResult.Ok },
|
||||
new object[] { 5f, -98d, HitResult.Meh },
|
||||
new object[] { 5f, -99d, HitResult.Meh },
|
||||
new object[] { 5f, -120d, HitResult.Meh },
|
||||
new object[] { 5f, -121d, HitResult.Meh },
|
||||
new object[] { 5f, -122d, HitResult.Miss },
|
||||
new object[] { 5f, -123d, HitResult.Miss },
|
||||
new object[] { 5f, 96d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 5f, 97d, HitResult.Miss },
|
||||
// new object[] { 5f, 98d, HitResult.Miss },
|
||||
// new object[] { 5f, 99d, HitResult.Miss },
|
||||
// new object[] { 5f, 120d, HitResult.Miss },
|
||||
// new object[] { 5f, 121d, HitResult.Miss },
|
||||
// new object[] { 5f, 122d, HitResult.Miss },
|
||||
// new object[] { 5f, 123d, HitResult.Miss },
|
||||
|
||||
// OD = 3.1 test cases.
|
||||
// PERFECT hit window is [ -16ms, 16ms]
|
||||
// GREAT hit window is [ -47ms, 47ms]
|
||||
// GOOD hit window is [ -77ms, 77ms]
|
||||
// OK hit window is [ -97ms, 97ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-121ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 3.1f, 15d, HitResult.Perfect },
|
||||
new object[] { 3.1f, 16d, HitResult.Perfect },
|
||||
new object[] { 3.1f, 17d, HitResult.Great },
|
||||
new object[] { 3.1f, 18d, HitResult.Great },
|
||||
new object[] { 3.1f, 46d, HitResult.Great },
|
||||
new object[] { 3.1f, 47d, HitResult.Great },
|
||||
new object[] { 3.1f, 48d, HitResult.Good },
|
||||
new object[] { 3.1f, 49d, HitResult.Good },
|
||||
new object[] { 3.1f, 76d, HitResult.Good },
|
||||
new object[] { 3.1f, 77d, HitResult.Good },
|
||||
new object[] { 3.1f, 78d, HitResult.Ok },
|
||||
new object[] { 3.1f, 79d, HitResult.Ok },
|
||||
new object[] { 3.1f, 96d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 3.1f, 97d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 98d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 99d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 120d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 121d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 122d, HitResult.Miss },
|
||||
// new object[] { 3.1f, 123d, HitResult.Miss },
|
||||
new object[] { 3.1f, -96d, HitResult.Ok },
|
||||
new object[] { 3.1f, -97d, HitResult.Ok },
|
||||
new object[] { 3.1f, -98d, HitResult.Meh },
|
||||
new object[] { 3.1f, -99d, HitResult.Meh },
|
||||
new object[] { 3.1f, -120d, HitResult.Meh },
|
||||
new object[] { 3.1f, -121d, HitResult.Meh },
|
||||
new object[] { 3.1f, -122d, HitResult.Miss },
|
||||
new object[] { 3.1f, -123d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] score_v1_non_convert_hard_rock_test_cases =
|
||||
{
|
||||
// OD = 5 test cases.
|
||||
// This leads to "effective" OD of 7.
|
||||
// PERFECT hit window is [-11ms, 11ms]
|
||||
// GREAT hit window is [-35ms, 35ms]
|
||||
// GOOD hit window is [-58ms, 58ms]
|
||||
// OK hit window is [-80ms, 80ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-97ms, ----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -10d, HitResult.Perfect },
|
||||
new object[] { 5f, -11d, HitResult.Perfect },
|
||||
new object[] { 5f, -12d, HitResult.Great },
|
||||
new object[] { 5f, -13d, HitResult.Great },
|
||||
new object[] { 5f, -34d, HitResult.Great },
|
||||
new object[] { 5f, -35d, HitResult.Great },
|
||||
new object[] { 5f, -36d, HitResult.Good },
|
||||
new object[] { 5f, -37d, HitResult.Good },
|
||||
new object[] { 5f, -57d, HitResult.Good },
|
||||
new object[] { 5f, -58d, HitResult.Good },
|
||||
new object[] { 5f, -59d, HitResult.Ok },
|
||||
new object[] { 5f, -60d, HitResult.Ok },
|
||||
new object[] { 5f, -79d, HitResult.Ok },
|
||||
new object[] { 5f, -80d, HitResult.Ok },
|
||||
new object[] { 5f, -81d, HitResult.Meh },
|
||||
new object[] { 5f, -82d, HitResult.Meh },
|
||||
new object[] { 5f, -96d, HitResult.Meh },
|
||||
new object[] { 5f, -97d, HitResult.Meh },
|
||||
new object[] { 5f, -98d, HitResult.Miss },
|
||||
new object[] { 5f, -99d, HitResult.Miss },
|
||||
new object[] { 5f, 79d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 5f, 80d, HitResult.Miss },
|
||||
// new object[] { 5f, 81d, HitResult.Miss },
|
||||
// new object[] { 5f, 82d, HitResult.Miss },
|
||||
// new object[] { 5f, 96d, HitResult.Miss },
|
||||
// new object[] { 5f, 97d, HitResult.Miss },
|
||||
// new object[] { 5f, 98d, HitResult.Miss },
|
||||
// new object[] { 5f, 99d, HitResult.Miss },
|
||||
|
||||
// OD = 9.3 test cases.
|
||||
// This leads to "effective" OD of 13.02.
|
||||
// Note that contrary to other rulesets this does NOT cap out to OD 10!
|
||||
// PERFECT hit window is [-11ms, 11ms]
|
||||
// GREAT hit window is [-25ms, 25ms]
|
||||
// GOOD hit window is [-49ms, 49ms]
|
||||
// OK hit window is [-70ms, 70ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-87ms, ----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 9.3f, 10d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 11d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 12d, HitResult.Great },
|
||||
new object[] { 9.3f, 13d, HitResult.Great },
|
||||
new object[] { 9.3f, 24d, HitResult.Great },
|
||||
new object[] { 9.3f, 25d, HitResult.Great },
|
||||
new object[] { 9.3f, 26d, HitResult.Good },
|
||||
new object[] { 9.3f, 27d, HitResult.Good },
|
||||
new object[] { 9.3f, 48d, HitResult.Good },
|
||||
new object[] { 9.3f, 49d, HitResult.Good },
|
||||
new object[] { 9.3f, 50d, HitResult.Ok },
|
||||
new object[] { 9.3f, 51d, HitResult.Ok },
|
||||
new object[] { 9.3f, 69d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 9.3f, 70d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 71d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 72d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 86d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 87d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 88d, HitResult.Miss },
|
||||
// new object[] { 9.3f, 89d, HitResult.Miss },
|
||||
new object[] { 9.3f, -69d, HitResult.Ok },
|
||||
new object[] { 9.3f, -70d, HitResult.Ok },
|
||||
new object[] { 9.3f, -71d, HitResult.Meh },
|
||||
new object[] { 9.3f, -72d, HitResult.Meh },
|
||||
new object[] { 9.3f, -86d, HitResult.Meh },
|
||||
new object[] { 9.3f, -87d, HitResult.Meh },
|
||||
new object[] { 9.3f, -88d, HitResult.Miss },
|
||||
new object[] { 9.3f, -89d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] score_v1_non_convert_easy_test_cases =
|
||||
{
|
||||
// Assume OD = 5 (other values are not tested, even OD 5 is enough to exercise the flooring logic).
|
||||
// PERFECT hit window is [ -22ms, 22ms]
|
||||
// GREAT hit window is [ -68ms, 68ms]
|
||||
// GOOD hit window is [-114ms, 114ms]
|
||||
// OK hit window is [-156ms, 156ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-190ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -21d, HitResult.Perfect },
|
||||
new object[] { 5f, -22d, HitResult.Perfect },
|
||||
new object[] { 5f, -23d, HitResult.Great },
|
||||
new object[] { 5f, -24d, HitResult.Great },
|
||||
new object[] { 5f, -67d, HitResult.Great },
|
||||
new object[] { 5f, -68d, HitResult.Great },
|
||||
new object[] { 5f, -69d, HitResult.Good },
|
||||
new object[] { 5f, -70d, HitResult.Good },
|
||||
new object[] { 5f, -113d, HitResult.Good },
|
||||
new object[] { 5f, -114d, HitResult.Good },
|
||||
new object[] { 5f, -115d, HitResult.Ok },
|
||||
new object[] { 5f, -116d, HitResult.Ok },
|
||||
new object[] { 5f, -155d, HitResult.Ok },
|
||||
new object[] { 5f, -156d, HitResult.Ok },
|
||||
new object[] { 5f, -157d, HitResult.Meh },
|
||||
new object[] { 5f, -158d, HitResult.Meh },
|
||||
new object[] { 5f, -189d, HitResult.Meh },
|
||||
new object[] { 5f, -190d, HitResult.Meh },
|
||||
new object[] { 5f, -191d, HitResult.Miss },
|
||||
new object[] { 5f, -192d, HitResult.Miss },
|
||||
new object[] { 5f, 155d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 5f, 156d, HitResult.Miss },
|
||||
// new object[] { 5f, 157d, HitResult.Miss },
|
||||
// new object[] { 5f, 158d, HitResult.Miss },
|
||||
// new object[] { 5f, 189d, HitResult.Miss },
|
||||
// new object[] { 5f, 190d, HitResult.Miss },
|
||||
// new object[] { 5f, 191d, HitResult.Miss },
|
||||
// new object[] { 5f, 192d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] score_v1_non_convert_double_time_test_cases =
|
||||
{
|
||||
// Assume OD = 5 (other values are not tested, even OD 5 is enough to exercise the flooring logic).
|
||||
// PERFECT hit window is [ -24ms, 24ms]
|
||||
// GREAT hit window is [ -73ms, 73ms]
|
||||
// GOOD hit window is [-123ms, 123ms]
|
||||
// OK hit window is [-168ms, 168ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-204ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -23d, HitResult.Perfect },
|
||||
new object[] { 5f, -24d, HitResult.Perfect },
|
||||
new object[] { 5f, -25d, HitResult.Great },
|
||||
new object[] { 5f, -26d, HitResult.Great },
|
||||
new object[] { 5f, -72d, HitResult.Great },
|
||||
new object[] { 5f, -73d, HitResult.Great },
|
||||
new object[] { 5f, -74d, HitResult.Good },
|
||||
new object[] { 5f, -75d, HitResult.Good },
|
||||
new object[] { 5f, -122d, HitResult.Good },
|
||||
new object[] { 5f, -123d, HitResult.Good },
|
||||
new object[] { 5f, -124d, HitResult.Ok },
|
||||
new object[] { 5f, -125d, HitResult.Ok },
|
||||
new object[] { 5f, -167d, HitResult.Ok },
|
||||
new object[] { 5f, -168d, HitResult.Ok },
|
||||
new object[] { 5f, -169d, HitResult.Meh },
|
||||
new object[] { 5f, -170d, HitResult.Meh },
|
||||
new object[] { 5f, -203d, HitResult.Meh },
|
||||
new object[] { 5f, -204d, HitResult.Meh },
|
||||
new object[] { 5f, -205d, HitResult.Miss },
|
||||
new object[] { 5f, -206d, HitResult.Miss },
|
||||
new object[] { 5f, 167d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 5f, 168d, HitResult.Miss },
|
||||
// new object[] { 5f, 169d, HitResult.Miss },
|
||||
// new object[] { 5f, 170d, HitResult.Miss },
|
||||
// new object[] { 5f, 203d, HitResult.Miss },
|
||||
// new object[] { 5f, 204d, HitResult.Miss },
|
||||
// new object[] { 5f, 205d, HitResult.Miss },
|
||||
// new object[] { 5f, 206d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] score_v1_non_convert_half_time_test_cases =
|
||||
{
|
||||
// Assume OD = 5 (other values are not tested, even OD 5 is enough to exercise the flooring logic).
|
||||
// PERFECT hit window is [ -12ms, 12ms]
|
||||
// GREAT hit window is [ -36ms, 36ms]
|
||||
// GOOD hit window is [ -61ms, 61ms]
|
||||
// OK hit window is [ -84ms, 84ms) <- not a typo, this side of the interval is OPEN!
|
||||
// MEH hit window is [-102ms, -----) <- it is NOT POSSIBLE to get a MEH result on a late hit!
|
||||
new object[] { 5f, -11d, HitResult.Perfect },
|
||||
new object[] { 5f, -12d, HitResult.Perfect },
|
||||
new object[] { 5f, -13d, HitResult.Great },
|
||||
new object[] { 5f, -14d, HitResult.Great },
|
||||
new object[] { 5f, -35d, HitResult.Great },
|
||||
new object[] { 5f, -36d, HitResult.Great },
|
||||
new object[] { 5f, -37d, HitResult.Good },
|
||||
new object[] { 5f, -38d, HitResult.Good },
|
||||
new object[] { 5f, -60d, HitResult.Good },
|
||||
new object[] { 5f, -61d, HitResult.Good },
|
||||
new object[] { 5f, -62d, HitResult.Ok },
|
||||
new object[] { 5f, -63d, HitResult.Ok },
|
||||
new object[] { 5f, -83d, HitResult.Ok },
|
||||
new object[] { 5f, -84d, HitResult.Ok },
|
||||
new object[] { 5f, -85d, HitResult.Meh },
|
||||
new object[] { 5f, -86d, HitResult.Meh },
|
||||
new object[] { 5f, -101d, HitResult.Meh },
|
||||
new object[] { 5f, -102d, HitResult.Meh },
|
||||
new object[] { 5f, -103d, HitResult.Miss },
|
||||
new object[] { 5f, -104d, HitResult.Miss },
|
||||
new object[] { 5f, 83d, HitResult.Ok },
|
||||
// coverage of broken "can't hit meh late" behaviour, which is intentionally not being reproduced
|
||||
// new object[] { 5f, 84d, HitResult.Miss },
|
||||
// new object[] { 5f, 85d, HitResult.Miss },
|
||||
// new object[] { 5f, 86d, HitResult.Miss },
|
||||
// new object[] { 5f, 101d, HitResult.Miss },
|
||||
// new object[] { 5f, 102d, HitResult.Miss },
|
||||
// new object[] { 5f, 103d, HitResult.Miss },
|
||||
// new object[] { 5f, 104d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private const double note_time = 300;
|
||||
|
||||
[TestCaseSource(nameof(score_v2_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV2(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createNonConvertBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new ManiaModScoreV2()]
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV2 single note @ OD{overallDifficulty}", beatmap, $@"SV2 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(score_v1_non_convert_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createNonConvertBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV1 single note @ OD{overallDifficulty}", beatmap, $@"SV1 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(score_v1_convert_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createConvertBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new ManiaModKey1()],
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(score_v1_non_convert_hard_rock_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV1AndHardRockNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createNonConvertBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new ManiaModHardRock()],
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV1+HR single note @ OD{overallDifficulty}", beatmap, $@"SV1+HR {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(score_v1_non_convert_easy_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV1AndEasyNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createNonConvertBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new ManiaModEasy()],
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV1+EZ single note @ OD{overallDifficulty}", beatmap, $@"SV1+EZ {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(score_v1_non_convert_double_time_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV1AndDoubleTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createNonConvertBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new ManiaModDoubleTime()],
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV1+DT single note @ OD{overallDifficulty}", beatmap, $@"SV1+DT {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(score_v1_non_convert_half_time_test_cases))]
|
||||
public void TestHitWindowTreatmentWithScoreV1AndHalfTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createNonConvertBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new ManiaModHalfTime()],
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"SV1+HT single note @ OD{overallDifficulty}", beatmap, $@"SV1+HT {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
private static ManiaBeatmap createNonConvertBeatmap(float overallDifficulty)
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(1))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = note_time,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
OverallDifficulty = overallDifficulty,
|
||||
CircleSize = 1,
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo,
|
||||
},
|
||||
ControlPointInfo = cpi,
|
||||
};
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
private static Beatmap createConvertBeatmap(float overallDifficulty)
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new FakeCircle
|
||||
{
|
||||
StartTime = note_time,
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
OverallDifficulty = overallDifficulty,
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new RulesetInfo { OnlineID = 0 }
|
||||
},
|
||||
ControlPointInfo = cpi,
|
||||
};
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
private class FakeCircle : HitObject, IHasPosition
|
||||
{
|
||||
public float X
|
||||
{
|
||||
get => Position.X;
|
||||
set => Position = new Vector2(value, Position.Y);
|
||||
}
|
||||
|
||||
public float Y
|
||||
{
|
||||
get => Position.Y;
|
||||
set => Position = new Vector2(Position.X, value);
|
||||
}
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,12 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() => toggleTouchControls(false));
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
InputManager.EndTouch(new Touch(TouchSource.Touch1, Vector2.Zero));
|
||||
InputManager.EndTouch(new Touch(TouchSource.Touch2, Vector2.Zero));
|
||||
toggleTouchControls(false);
|
||||
});
|
||||
|
||||
#region Without touch controls
|
||||
|
||||
@@ -71,6 +76,35 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBetweenTwoColumns()
|
||||
{
|
||||
AddStep("touch after column 0", () =>
|
||||
{
|
||||
var column = getColumn(0);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(column.LayoutSize.X + 0.5f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
AddAssert("column 0 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(0).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 0 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
AddStep("touch before column 1", () =>
|
||||
{
|
||||
var column = getColumn(1);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(-0.5f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
AddAssert("column 1 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(1).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 1 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(1).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region With touch controls
|
||||
@@ -132,6 +166,38 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchControlBetweenTwoColumns()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
|
||||
AddStep("touch after receptor 0", () =>
|
||||
{
|
||||
var column = getReceptor(0);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(column.LayoutSize.X + 1f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
|
||||
AddAssert("column 0 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(0).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 0 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getReceptor(0).Action.Value));
|
||||
AddStep("touch before receptor 1", () =>
|
||||
{
|
||||
var column = getReceptor(1);
|
||||
InputManager.BeginTouch(new Touch(TouchSource.Touch1, column.ToScreenSpace(new Vector2(-1f, column.LayoutSize.Y / 2))));
|
||||
});
|
||||
AddAssert("column 1 pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(1).Action.Value));
|
||||
AddStep("release finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
AddAssert("column 1 released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getReceptor(1).Action.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void toggleTouchControls(bool enabled)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new ManiaBeatmap(new StageDefinition(1))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note { StartTime = 0, },
|
||||
new Note { StartTime = 5000, },
|
||||
new Note { StartTime = 10000, },
|
||||
new Note { StartTime = 15000, }
|
||||
},
|
||||
Difficulty = { CircleSize = 1 },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = ruleset,
|
||||
}
|
||||
};
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
|
||||
[Test]
|
||||
public void TestRecording()
|
||||
{
|
||||
seekTo(0);
|
||||
AddStep("press space", () => InputManager.PressKey(Key.Space));
|
||||
seekTo(15);
|
||||
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
|
||||
AddUntilStep("button press recorded to replay", () => Player.Score.Replay.Frames.OfType<ManiaReplayFrame>().Any(f => f.Actions.SequenceEqual([ManiaAction.Key1])));
|
||||
}
|
||||
|
||||
private void seekTo(double time)
|
||||
{
|
||||
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneReplayRewinding : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private ReplayPlayer currentPlayer = null!;
|
||||
|
||||
[Test]
|
||||
public void TestRewindingToMiddleOfHoldNote()
|
||||
{
|
||||
Score score = null!;
|
||||
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 500,
|
||||
EndTime = 1500,
|
||||
Column = 2
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep(@"create replay", () => score = new Score
|
||||
{
|
||||
Replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(500, ManiaAction.Key3),
|
||||
new ManiaReplayFrame(1500),
|
||||
}
|
||||
},
|
||||
ScoreInfo = new ScoreInfo()
|
||||
});
|
||||
|
||||
AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(beatmap));
|
||||
AddStep(@"set ruleset", () => Ruleset.Value = beatmap.BeatmapInfo.Ruleset);
|
||||
AddStep(@"push player", () => LoadScreen(currentPlayer = new ReplayPlayer(score)));
|
||||
|
||||
AddUntilStep(@"wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep(@"wait for hold to be judged", () => currentPlayer.ChildrenOfType<IFrameStableClock>().Single().CurrentTime, () => Is.GreaterThan(1600));
|
||||
AddStep(@"seek to middle of hold note", () => currentPlayer.Seek(1000));
|
||||
AddUntilStep(@"wait for gameplay to complete", () => currentPlayer.GameplayState.HasCompleted);
|
||||
AddAssert(@"no misses registered", () => currentPlayer.GameplayState.ScoreProcessor.Statistics.GetValueOrDefault(HitResult.Miss), () => Is.Zero);
|
||||
|
||||
AddStep(@"exit player", () => currentPlayer.Exit());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectComboAccountingForConcurrentObjects()
|
||||
{
|
||||
Score score = null!;
|
||||
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = 500,
|
||||
Column = 0,
|
||||
},
|
||||
new Note
|
||||
{
|
||||
StartTime = 500,
|
||||
Column = 2,
|
||||
},
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = 1000,
|
||||
EndTime = 1500,
|
||||
Column = 1,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AddStep(@"create replay", () => score = new Score
|
||||
{
|
||||
Replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(500, ManiaAction.Key1, ManiaAction.Key3),
|
||||
new ManiaReplayFrame(520),
|
||||
new ManiaReplayFrame(1000, ManiaAction.Key2),
|
||||
new ManiaReplayFrame(1500),
|
||||
}
|
||||
},
|
||||
ScoreInfo = new ScoreInfo()
|
||||
});
|
||||
|
||||
AddStep(@"set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(beatmap));
|
||||
AddStep(@"set ruleset", () => Ruleset.Value = beatmap.BeatmapInfo.Ruleset);
|
||||
AddStep(@"push player", () => LoadScreen(currentPlayer = new ReplayPlayer(score)));
|
||||
|
||||
AddUntilStep(@"wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep(@"wait for objects to be judged", () => currentPlayer.ChildrenOfType<IFrameStableClock>().Single().CurrentTime, () => Is.GreaterThan(1600));
|
||||
AddStep(@"stop gameplay", () => currentPlayer.ChildrenOfType<GameplayClockContainer>().Single().Stop());
|
||||
AddStep(@"seek to start", () => currentPlayer.Seek(0));
|
||||
AddAssert(@"combo is 0", () => currentPlayer.GameplayState.ScoreProcessor.Combo.Value, () => Is.Zero);
|
||||
|
||||
AddStep(@"exit player", () => currentPlayer.Exit());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneReplayStability : ReplayStabilityTestScene
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// PERFECT hit window is [ -19.5ms, 19.5ms]
|
||||
// GREAT hit window is [ -49.5ms, 49.5ms]
|
||||
// GOOD hit window is [ -82.5ms, 82.5ms]
|
||||
// OK hit window is [-112.5ms, 112.5ms]
|
||||
// MEH hit window is [-136.5ms, 136.5ms]
|
||||
// MISS hit window is [-173.5ms, 173.5ms]
|
||||
new object[] { 5f, -19d, HitResult.Perfect },
|
||||
new object[] { 5f, -19.2d, HitResult.Perfect },
|
||||
new object[] { 5f, -19.7d, HitResult.Great },
|
||||
new object[] { 5f, -20d, HitResult.Great },
|
||||
new object[] { 5f, -48d, HitResult.Great },
|
||||
new object[] { 5f, -48.4d, HitResult.Great },
|
||||
new object[] { 5f, -48.7d, HitResult.Great },
|
||||
new object[] { 5f, -49d, HitResult.Great },
|
||||
new object[] { 5f, -49.2d, HitResult.Great },
|
||||
new object[] { 5f, -49.7d, HitResult.Good },
|
||||
new object[] { 5f, -50d, HitResult.Good },
|
||||
new object[] { 5f, -81d, HitResult.Good },
|
||||
new object[] { 5f, -81.2d, HitResult.Good },
|
||||
new object[] { 5f, -81.7d, HitResult.Good },
|
||||
new object[] { 5f, -82d, HitResult.Good },
|
||||
new object[] { 5f, -82.2d, HitResult.Good },
|
||||
new object[] { 5f, -82.7d, HitResult.Ok },
|
||||
new object[] { 5f, -83d, HitResult.Ok },
|
||||
new object[] { 5f, -111d, HitResult.Ok },
|
||||
new object[] { 5f, -111.2d, HitResult.Ok },
|
||||
new object[] { 5f, -111.7d, HitResult.Ok },
|
||||
new object[] { 5f, -112d, HitResult.Ok },
|
||||
new object[] { 5f, -112.2d, HitResult.Ok },
|
||||
new object[] { 5f, -112.7d, HitResult.Meh },
|
||||
new object[] { 5f, -113d, HitResult.Meh },
|
||||
new object[] { 5f, -135d, HitResult.Meh },
|
||||
new object[] { 5f, -135.2d, HitResult.Meh },
|
||||
new object[] { 5f, -135.8d, HitResult.Meh },
|
||||
new object[] { 5f, -136d, HitResult.Meh },
|
||||
new object[] { 5f, -136.2d, HitResult.Meh },
|
||||
new object[] { 5f, -136.7d, HitResult.Miss },
|
||||
new object[] { 5f, -137d, HitResult.Miss },
|
||||
|
||||
// OD = 9.3 test cases.
|
||||
// PERFECT hit window is [ -14.5ms, 14.5ms]
|
||||
// GREAT hit window is [ -36.5ms, 36.5ms]
|
||||
// GOOD hit window is [ -69.5ms, 69.5ms]
|
||||
// OK hit window is [ -99.5ms, 99.5ms]
|
||||
// MEH hit window is [-123.5ms, 123.5ms]
|
||||
// MISS hit window is [-160.5ms, 160.5ms]
|
||||
new object[] { 9.3f, 14d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 14.2d, HitResult.Perfect },
|
||||
new object[] { 9.3f, 14.7d, HitResult.Great },
|
||||
new object[] { 9.3f, 15d, HitResult.Great },
|
||||
new object[] { 9.3f, 35d, HitResult.Great },
|
||||
new object[] { 9.3f, 35.3d, HitResult.Great },
|
||||
new object[] { 9.3f, 35.8d, HitResult.Great },
|
||||
new object[] { 9.3f, 36.3d, HitResult.Great },
|
||||
new object[] { 9.3f, 36.7d, HitResult.Good },
|
||||
new object[] { 9.3f, 37d, HitResult.Good },
|
||||
new object[] { 9.3f, 68d, HitResult.Good },
|
||||
new object[] { 9.3f, 68.4d, HitResult.Good },
|
||||
new object[] { 9.3f, 68.9d, HitResult.Good },
|
||||
new object[] { 9.3f, 69.25d, HitResult.Good },
|
||||
new object[] { 9.3f, 69.85d, HitResult.Ok },
|
||||
new object[] { 9.3f, 70d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98.3d, HitResult.Ok },
|
||||
new object[] { 9.3f, 98.6d, HitResult.Ok },
|
||||
new object[] { 9.3f, 99d, HitResult.Ok },
|
||||
new object[] { 9.3f, 99.3d, HitResult.Ok },
|
||||
new object[] { 9.3f, 99.7d, HitResult.Meh },
|
||||
new object[] { 9.3f, 100d, HitResult.Meh },
|
||||
new object[] { 9.3f, 122d, HitResult.Meh },
|
||||
new object[] { 9.3f, 122.34d, HitResult.Meh },
|
||||
new object[] { 9.3f, 122.57d, HitResult.Meh },
|
||||
new object[] { 9.3f, 123.45d, HitResult.Meh },
|
||||
new object[] { 9.3f, 123.95d, HitResult.Miss },
|
||||
new object[] { 9.3f, 124d, HitResult.Miss },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double note_time = 300;
|
||||
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition(1))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note
|
||||
{
|
||||
StartTime = note_time,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
OverallDifficulty = overallDifficulty,
|
||||
CircleSize = 1,
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo,
|
||||
},
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new ManiaReplayFrame(0),
|
||||
new ManiaReplayFrame(note_time + hitOffset, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(note_time + hitOffset + 20),
|
||||
}
|
||||
};
|
||||
|
||||
RunTest(beatmap, replay, [expectedResult]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
@@ -36,20 +37,23 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
{
|
||||
int notes = HitObjects.Count(s => s is Note);
|
||||
int holdNotes = HitObjects.Count(s => s is HoldNote);
|
||||
int sum = Math.Max(1, notes + holdNotes);
|
||||
|
||||
return new[]
|
||||
{
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Note Count",
|
||||
Name = BeatmapStatisticStrings.Notes,
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
|
||||
Content = notes.ToString(),
|
||||
BarDisplayLength = notes / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Hold Note Count",
|
||||
Name = BeatmapStatisticStrings.HoldNotes,
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
|
||||
Content = holdNotes.ToString(),
|
||||
BarDisplayLength = holdNotes / (float)sum,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Configuration.Tracking;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Localisation;
|
||||
@@ -25,17 +24,6 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
|
||||
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
|
||||
|
||||
#pragma warning disable CS0618
|
||||
// Although obsolete, this is still required to populate the bindable from the database in case migration is required.
|
||||
SetDefault<double?>(ManiaRulesetSetting.ScrollTime, null);
|
||||
|
||||
if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime)
|
||||
{
|
||||
SetValue(ManiaRulesetSetting.ScrollSpeed, Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
|
||||
SetValue<double?>(ManiaRulesetSetting.ScrollTime, null);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||
@@ -52,8 +40,6 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
|
||||
public enum ManiaRulesetSetting
|
||||
{
|
||||
[Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30
|
||||
ScrollTime,
|
||||
ScrollSpeed,
|
||||
ScrollDirection,
|
||||
TimingBasedNoteColouring,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Checks
|
||||
{
|
||||
public class CheckManiaConcurrentObjects : CheckConcurrentObjects
|
||||
{
|
||||
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
{
|
||||
var hitobject = hitObjects[i];
|
||||
|
||||
for (int j = i + 1; j < hitObjects.Count; ++j)
|
||||
{
|
||||
var nextHitobject = hitObjects[j];
|
||||
|
||||
// Mania hitobjects are only considered concurrent if they also share the same column.
|
||||
if ((hitobject as IHasColumn)?.Column != (nextHitobject as IHasColumn)?.Column)
|
||||
continue;
|
||||
|
||||
// Two hitobjects cannot be concurrent without also being concurrent with all objects in between.
|
||||
// So if the next object is not concurrent, then we know no future objects will be either.
|
||||
if (!AreConcurrent(hitobject, nextHitobject))
|
||||
break;
|
||||
|
||||
if (hitobject.GetType() == nextHitobject.GetType())
|
||||
yield return new IssueTemplateConcurrentSame(this).Create(hitobject, nextHitobject);
|
||||
else
|
||||
yield return new IssueTemplateConcurrentDifferent(this).Create(hitobject, nextHitobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
// Compose
|
||||
new CheckManiaConcurrentObjects(),
|
||||
|
||||
// Settings
|
||||
new CheckKeyCount(),
|
||||
new CheckManiaAbnormalDifficultySettings(),
|
||||
|
||||
@@ -89,6 +89,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
@@ -103,6 +104,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
KeyboardStep = 1,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
public class HoldNoteJudgementResult : JudgementResult
|
||||
{
|
||||
private Stack<(double time, bool holding)> holdingState { get; } = new Stack<(double, bool)>();
|
||||
|
||||
public HoldNoteJudgementResult(HoldNote hitObject, Judgement judgement)
|
||||
: base(hitObject, judgement)
|
||||
{
|
||||
holdingState.Push((double.NegativeInfinity, false));
|
||||
}
|
||||
|
||||
private (double time, bool holding) getLastReport(double currentTime)
|
||||
{
|
||||
while (holdingState.Peek().time > currentTime)
|
||||
holdingState.Pop();
|
||||
|
||||
return holdingState.Peek();
|
||||
}
|
||||
|
||||
public bool IsHolding(double currentTime) => getLastReport(currentTime).holding;
|
||||
|
||||
public bool DroppedHoldAfter(double time)
|
||||
{
|
||||
foreach (var state in holdingState)
|
||||
{
|
||||
if (state.time >= time && !state.holding)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ReportHoldState(double currentTime, bool holding)
|
||||
{
|
||||
var lastReport = getLastReport(currentTime);
|
||||
if (holding != lastReport.holding)
|
||||
holdingState.Push((currentTime, holding));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
@@ -17,20 +18,72 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class ManiaFilterCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
private FilterCriteria.OptionalRange<float> keys;
|
||||
private readonly HashSet<int> includedKeyCounts = Enumerable.Range(1, LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT).ToHashSet();
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo, FilterCriteria criteria)
|
||||
{
|
||||
return !keys.HasFilter || keys.IsInRange(ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods));
|
||||
int keyCount = ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), criteria.Mods);
|
||||
|
||||
return includedKeyCounts.Contains(keyCount);
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string strValues)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "key":
|
||||
case "keys":
|
||||
return FilterQueryParser.TryUpdateCriteriaRange(ref keys, op, value);
|
||||
{
|
||||
var keyCounts = new HashSet<int>();
|
||||
|
||||
foreach (string strValue in strValues.Split(','))
|
||||
{
|
||||
if (!int.TryParse(strValue, out int keyCount))
|
||||
return false;
|
||||
|
||||
keyCounts.Add(keyCount);
|
||||
}
|
||||
|
||||
int? singleKeyCount = keyCounts.Count == 1 ? keyCounts.Single() : null;
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case Operator.Equal:
|
||||
includedKeyCounts.IntersectWith(keyCounts);
|
||||
return true;
|
||||
|
||||
case Operator.NotEqual:
|
||||
includedKeyCounts.ExceptWith(keyCounts);
|
||||
return true;
|
||||
|
||||
case Operator.Less:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k >= singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
case Operator.LessOrEqual:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k > singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
case Operator.Greater:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k <= singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
case Operator.GreaterOrEqual:
|
||||
if (singleKeyCount == null) return false;
|
||||
|
||||
includedKeyCounts.RemoveWhere(k => k < singleKeyCount.Value);
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -38,7 +91,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public bool FilterMayChangeFromMods(ValueChangedEvent<IReadOnlyList<Mod>> mods)
|
||||
{
|
||||
if (keys.HasFilter)
|
||||
if (includedKeyCounts.Count != LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT)
|
||||
{
|
||||
// Interpreting as the Mod type is required for equality comparison.
|
||||
HashSet<Mod> oldSet = mods.OldValue.OfType<ManiaKeyMod>().AsEnumerable<Mod>().ToHashSet();
|
||||
|
||||
@@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
yield return new ManiaModMirror();
|
||||
|
||||
if (mods.HasFlag(LegacyMods.ScoreV2))
|
||||
yield return new ModScoreV2();
|
||||
yield return new ManiaModScoreV2();
|
||||
}
|
||||
|
||||
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
|
||||
@@ -296,7 +296,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
case ModType.System:
|
||||
return new Mod[]
|
||||
{
|
||||
new ModScoreV2(),
|
||||
new ManiaModScoreV2(),
|
||||
};
|
||||
|
||||
default:
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
@@ -17,29 +15,21 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
/// <remarks>
|
||||
/// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same.
|
||||
/// </remarks>
|
||||
public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject
|
||||
public interface IManiaRateAdjustmentMod : IApplicableToHitObject
|
||||
{
|
||||
BindableNumber<double> SpeedChange { get; }
|
||||
|
||||
HitWindows HitWindows { get; set; }
|
||||
|
||||
void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
HitWindows = new ManiaHitWindows(SpeedChange.Value);
|
||||
HitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||
}
|
||||
|
||||
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case Note:
|
||||
hitObject.HitWindows = HitWindows;
|
||||
((ManiaHitWindows)hitObject.HitWindows).SpeedMultiplier = SpeedChange.Value;
|
||||
break;
|
||||
|
||||
case HoldNote hold:
|
||||
hold.Head.HitWindows = HitWindows;
|
||||
hold.Tail.HitWindows = HitWindows;
|
||||
((ManiaHitWindows)hold.Head.HitWindows).SpeedMultiplier = SpeedChange.Value;
|
||||
((ManiaHitWindows)hold.Tail.HitWindows).SpeedMultiplier = SpeedChange.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,41 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModClassic : ModClassic
|
||||
public class ManiaModClassic : ModClassic, IApplicableToBeatmap
|
||||
{
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
bool isConvert = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
{
|
||||
switch (ho)
|
||||
{
|
||||
case Note note:
|
||||
{
|
||||
var hitWindows = (ManiaHitWindows)note.HitWindows;
|
||||
hitWindows.IsConvert = isConvert;
|
||||
hitWindows.ClassicModActive = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case HoldNote hold:
|
||||
{
|
||||
var headWindows = (ManiaHitWindows)hold.Head.HitWindows;
|
||||
var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows;
|
||||
headWindows.IsConvert = tailWindows.IsConvert = isConvert;
|
||||
headWindows.ClassicModActive = tailWindows.ClassicModActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public override bool Ranked => false;
|
||||
|
||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||
|
||||
[SettingSource("Coverage", "The proportion of playfield height that notes will be hidden for.")]
|
||||
public override BindableNumber<float> Coverage { get; } = new BindableFloat(0.5f)
|
||||
{
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
|
||||
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
|
||||
// make the map harder and is more of a personal preference.
|
||||
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
|
||||
|
||||
@@ -2,12 +2,32 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModEasy : ModEasyWithExtraLives
|
||||
public class ManiaModEasy : ModEasyWithExtraLives, IApplicableToHitObject
|
||||
{
|
||||
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
|
||||
|
||||
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
const double multiplier = 1 / 1.4;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case Note:
|
||||
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
|
||||
break;
|
||||
|
||||
case HoldNote hold:
|
||||
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
|
||||
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public override string Acronym => "FI";
|
||||
public override LocalisableString Description => @"Keys appear out of nowhere!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool ValidForFreestyleAsRequiredMod => false;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
|
||||
{
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModHardRock : ModHardRock
|
||||
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
|
||||
{
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => false;
|
||||
|
||||
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
const double multiplier = 1.4;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case Note:
|
||||
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
|
||||
break;
|
||||
|
||||
case HoldNote hold:
|
||||
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
|
||||
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
|
||||
{
|
||||
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
|
||||
|
||||
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
|
||||
// make the map any harder and is more of a personal preference.
|
||||
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
// apply perfect once the tail is reached
|
||||
if (HoldNote.HoldStartTime != null && timeOffset >= 0)
|
||||
if (HoldNote.IsHolding.Value && timeOffset >= 0)
|
||||
ApplyResult(GetCappedResult(HitResult.Perfect));
|
||||
else
|
||||
base.CheckForResult(userTriggered, timeOffset);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -9,13 +11,16 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModPerfect : ModPerfect
|
||||
{
|
||||
[SettingSource("Require perfect hits")]
|
||||
public BindableBool RequirePerfectHits { get; } = new BindableBool();
|
||||
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||
{
|
||||
if (!isRelevantResult(result.Judgement.MinResult) && !isRelevantResult(result.Judgement.MaxResult) && !isRelevantResult(result.Type))
|
||||
return false;
|
||||
|
||||
// Mania allows imperfect "Great" hits without failing.
|
||||
if (result.Judgement.MaxResult == HitResult.Perfect)
|
||||
if (result.Judgement.MaxResult == HitResult.Perfect && !RequirePerfectHits.Value)
|
||||
return result.Type < HitResult.Great;
|
||||
|
||||
return result.Type != result.Judgement.MaxResult;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Mods
|
||||
{
|
||||
public class ManiaModScoreV2 : ModScoreV2, IApplicableToBeatmap
|
||||
{
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
{
|
||||
switch (ho)
|
||||
{
|
||||
case Note note:
|
||||
{
|
||||
var hitWindows = (ManiaHitWindows)note.HitWindows;
|
||||
hitWindows.ScoreV2Active = true;
|
||||
break;
|
||||
}
|
||||
|
||||
case HoldNote hold:
|
||||
{
|
||||
var headWindows = (ManiaHitWindows)hold.Head.HitWindows;
|
||||
var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows;
|
||||
headWindows.ScoreV2Active = tailWindows.ScoreV2Active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@@ -29,9 +31,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public IBindable<bool> IsHitting => isHitting;
|
||||
public IBindable<bool> IsHolding => isHolding;
|
||||
|
||||
private readonly Bindable<bool> isHitting = new Bindable<bool>();
|
||||
private readonly Bindable<bool> isHolding = new Bindable<bool>();
|
||||
|
||||
public DrawableHoldNoteHead Head => headContainer.Child;
|
||||
public DrawableHoldNoteTail Tail => tailContainer.Child;
|
||||
@@ -55,16 +57,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
private SkinnableDrawable bodyPiece;
|
||||
|
||||
/// <summary>
|
||||
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
|
||||
/// </summary>
|
||||
public double? HoldStartTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to decide whether to visually clamp the hold note to the judgement line.
|
||||
/// </summary>
|
||||
private double? releaseTime;
|
||||
|
||||
public DrawableHoldNote()
|
||||
: this(null)
|
||||
{
|
||||
@@ -126,7 +118,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
isHitting.BindValueChanged(updateSlidingSample, true);
|
||||
isHolding.BindValueChanged(updateSlidingSample, true);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
@@ -134,8 +126,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
base.OnApply();
|
||||
|
||||
sizingContainer.Size = Vector2.One;
|
||||
HoldStartTime = null;
|
||||
releaseTime = null;
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
@@ -207,6 +197,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
public override void OnKilled()
|
||||
{
|
||||
base.OnKilled();
|
||||
// flush the final state of holding on kill.
|
||||
// this matters because some skin implementations like legacy skin
|
||||
// insert drawables in the hierarchy that are not a child of this DHO
|
||||
// (see `LegacyBodyPiece` and related machinations with `lightContainer` being added at column level)
|
||||
isHolding.Value = Result.IsHolding(Time.Current);
|
||||
(bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
|
||||
}
|
||||
|
||||
@@ -214,11 +209,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (Time.Current < releaseTime)
|
||||
releaseTime = null;
|
||||
|
||||
if (Time.Current < HoldStartTime)
|
||||
endHold();
|
||||
isHolding.Value = Result.IsHolding(Time.Current);
|
||||
|
||||
// Pad the full size container so its contents (i.e. the masking container) reach under the tail.
|
||||
// This is required for the tail to not be masked away, since it lies outside the bounds of the hold note.
|
||||
@@ -249,7 +240,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
//
|
||||
// As per stable, this should not apply for early hits, waiting until the object starts to touch the
|
||||
// judgement area first.
|
||||
if (Head.IsHit && releaseTime == null && DrawHeight > 0)
|
||||
if (Head.IsHit && !Result.DroppedHoldAfter(HitObject.StartTime) && DrawHeight > 0)
|
||||
{
|
||||
// How far past the hit target this hold note is.
|
||||
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;
|
||||
@@ -260,6 +251,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
sizingContainer.Height = 1;
|
||||
}
|
||||
|
||||
protected override JudgementResult CreateResult(Judgement judgement) => new HoldNoteJudgementResult(HitObject, judgement);
|
||||
|
||||
public new HoldNoteJudgementResult Result => (HoldNoteJudgementResult)base.Result;
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (Tail.AllJudged)
|
||||
@@ -274,7 +269,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
Body.TriggerResult(Tail.IsHit);
|
||||
|
||||
// Important that this is always called when a result is applied.
|
||||
endHold();
|
||||
Result.ReportHoldState(Time.Current, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +278,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
base.MissForcefully();
|
||||
|
||||
// Important that this is always called when a result is applied.
|
||||
endHold();
|
||||
Result.ReportHoldState(Time.Current, false);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<ManiaAction> e)
|
||||
@@ -317,8 +312,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (timeOffset < -Head.HitObject.HitWindows.WindowFor(HitResult.Miss))
|
||||
return;
|
||||
|
||||
HoldStartTime = Time.Current;
|
||||
isHitting.Value = true;
|
||||
Result.ReportHoldState(Time.Current, true);
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<ManiaAction> e)
|
||||
@@ -337,22 +331,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
// the user has released too early (before the tail).
|
||||
//
|
||||
// In such a case, we want to record this against the DrawableHoldNoteBody.
|
||||
if (HoldStartTime != null)
|
||||
if (isHolding.Value)
|
||||
{
|
||||
Tail.UpdateResult();
|
||||
Body.TriggerResult(Tail.IsHit);
|
||||
|
||||
endHold();
|
||||
releaseTime = Time.Current;
|
||||
Result.ReportHoldState(Time.Current, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void endHold()
|
||||
{
|
||||
HoldStartTime = null;
|
||||
isHitting.Value = false;
|
||||
}
|
||||
|
||||
protected override void LoadSamples()
|
||||
{
|
||||
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays.Legacy;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
@@ -47,5 +48,8 @@ namespace osu.Game.Rulesets.Mania.Replays
|
||||
|
||||
return new LegacyReplayFrame(Time, keys, null, ReplayButtonState.None);
|
||||
}
|
||||
|
||||
public override bool IsEquivalentTo(ReplayFrame other)
|
||||
=> other is ManiaReplayFrame maniaFrame && Time == maniaFrame.Time && Actions.SequenceEqual(maniaFrame.Actions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,107 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using System;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
public class ManiaHitWindows : HitWindows
|
||||
{
|
||||
private readonly double multiplier;
|
||||
private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D);
|
||||
private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34);
|
||||
private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67);
|
||||
private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97);
|
||||
private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121);
|
||||
private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158);
|
||||
|
||||
public ManiaHitWindows()
|
||||
: this(1)
|
||||
private double speedMultiplier = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier used to compensate for the playback speed of the track speeding up or slowing down.
|
||||
/// The goal of this multiplier is to keep hit windows independent of track speed.
|
||||
/// <list type="bullet">
|
||||
/// <item>When the track speed is above 1, the hit window ranges are multiplied by <see cref="SpeedMultiplier"/>, because the time elapses faster.</item>
|
||||
/// <item>When the track speed is below 1, the hit window ranges are also multiplied by <see cref="SpeedMultiplier"/>, because the time elapses slower.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public double SpeedMultiplier
|
||||
{
|
||||
get => speedMultiplier;
|
||||
set
|
||||
{
|
||||
speedMultiplier = value;
|
||||
updateWindows();
|
||||
}
|
||||
}
|
||||
|
||||
public ManiaHitWindows(double multiplier)
|
||||
private double difficultyMultiplier = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier used to make the gameplay more or less difficult.
|
||||
/// <list type="bullet">
|
||||
/// <item>When the <see cref="DifficultyMultiplier"/> is above 1, the hit windows decrease to make the gameplay harder.</item>
|
||||
/// <item>When the <see cref="DifficultyMultiplier"/> is below 1, the hit windows increase to make the gameplay easier.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public double DifficultyMultiplier
|
||||
{
|
||||
this.multiplier = multiplier;
|
||||
get => difficultyMultiplier;
|
||||
set
|
||||
{
|
||||
difficultyMultiplier = value;
|
||||
updateWindows();
|
||||
}
|
||||
}
|
||||
|
||||
private double totalMultiplier => speedMultiplier / difficultyMultiplier;
|
||||
|
||||
private double overallDifficulty;
|
||||
|
||||
private bool classicModActive;
|
||||
|
||||
public bool ClassicModActive
|
||||
{
|
||||
get => classicModActive;
|
||||
set
|
||||
{
|
||||
classicModActive = value;
|
||||
updateWindows();
|
||||
}
|
||||
}
|
||||
|
||||
private bool scoreV2Active;
|
||||
|
||||
public bool ScoreV2Active
|
||||
{
|
||||
get => scoreV2Active;
|
||||
set
|
||||
{
|
||||
scoreV2Active = value;
|
||||
updateWindows();
|
||||
}
|
||||
}
|
||||
|
||||
private bool isConvert;
|
||||
|
||||
public bool IsConvert
|
||||
{
|
||||
get => isConvert;
|
||||
set
|
||||
{
|
||||
isConvert = value;
|
||||
updateWindows();
|
||||
}
|
||||
}
|
||||
|
||||
private double perfect;
|
||||
private double great;
|
||||
private double good;
|
||||
private double ok;
|
||||
private double meh;
|
||||
private double miss;
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
@@ -36,11 +118,73 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => base.GetRanges().Select(r =>
|
||||
new DifficultyRange(
|
||||
r.Result,
|
||||
r.Min * multiplier,
|
||||
r.Average * multiplier,
|
||||
r.Max * multiplier)).ToArray();
|
||||
public override void SetDifficulty(double difficulty)
|
||||
{
|
||||
overallDifficulty = difficulty;
|
||||
updateWindows();
|
||||
}
|
||||
|
||||
private void updateWindows()
|
||||
{
|
||||
if (ClassicModActive && !ScoreV2Active)
|
||||
{
|
||||
if (IsConvert)
|
||||
{
|
||||
perfect = Math.Floor(16 * totalMultiplier) + 0.5;
|
||||
great = Math.Floor((Math.Round(overallDifficulty) > 4 ? 34 : 47) * totalMultiplier) + 0.5;
|
||||
good = Math.Floor((Math.Round(overallDifficulty) > 4 ? 67 : 77) * totalMultiplier) + 0.5;
|
||||
ok = Math.Floor(97 * totalMultiplier) + 0.5;
|
||||
meh = Math.Floor(121 * totalMultiplier) + 0.5;
|
||||
miss = Math.Floor(158 * totalMultiplier) + 0.5;
|
||||
}
|
||||
else
|
||||
{
|
||||
double invertedOd = Math.Clamp(10 - overallDifficulty, 0, 10);
|
||||
|
||||
perfect = Math.Floor(16 * totalMultiplier) + 0.5;
|
||||
great = Math.Floor((34 + 3 * invertedOd) * totalMultiplier) + 0.5;
|
||||
good = Math.Floor((67 + 3 * invertedOd) * totalMultiplier) + 0.5;
|
||||
ok = Math.Floor((97 + 3 * invertedOd) * totalMultiplier) + 0.5;
|
||||
meh = Math.Floor((121 + 3 * invertedOd) * totalMultiplier) + 0.5;
|
||||
miss = Math.Floor((158 + 3 * invertedOd) * totalMultiplier) + 0.5;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, perfect_window_range) * totalMultiplier) + 0.5;
|
||||
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, great_window_range) * totalMultiplier) + 0.5;
|
||||
good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, good_window_range) * totalMultiplier) + 0.5;
|
||||
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, ok_window_range) * totalMultiplier) + 0.5;
|
||||
meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, meh_window_range) * totalMultiplier) + 0.5;
|
||||
miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, miss_window_range) * totalMultiplier) + 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
public override double WindowFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Perfect:
|
||||
return perfect;
|
||||
|
||||
case HitResult.Great:
|
||||
return great;
|
||||
|
||||
case HitResult.Good:
|
||||
return good;
|
||||
|
||||
case HitResult.Ok:
|
||||
return ok;
|
||||
|
||||
case HitResult.Meh:
|
||||
return meh;
|
||||
|
||||
case HitResult.Miss:
|
||||
return miss;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
AccentColour.BindTo(holdNote.AccentColour);
|
||||
hittingLayer.AccentColour.BindTo(holdNote.AccentColour);
|
||||
((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNote.IsHitting);
|
||||
((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNote.IsHolding);
|
||||
}
|
||||
|
||||
AccentColour.BindValueChanged(colour =>
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
hittingLayer.AccentColour.BindTo(holdNoteTail.HoldNote.AccentColour);
|
||||
|
||||
hittingLayer.IsHitting.UnbindBindings();
|
||||
((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNoteTail.HoldNote.IsHitting);
|
||||
((IBindable<bool>)hittingLayer.IsHitting).BindTo(holdNoteTail.HoldNote.IsHolding);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
|
||||
@@ -40,9 +40,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
|
||||
if (leaderboard != null)
|
||||
leaderboard.Position = new Vector2(36, 115);
|
||||
|
||||
if (combo != null)
|
||||
{
|
||||
combo.ShowLabel.Value = false;
|
||||
@@ -55,6 +59,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
spectatorList.Position = new Vector2(36, -66);
|
||||
})
|
||||
{
|
||||
new DrawableGameplayLeaderboard(),
|
||||
new ArgonManiaComboCounter(),
|
||||
new SpectatorList
|
||||
{
|
||||
@@ -131,8 +136,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
switch (maniaLookup.Lookup)
|
||||
{
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnSpacing:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(2));
|
||||
case LegacyManiaSkinConfigurationLookups.LeftColumnSpacing:
|
||||
case LegacyManiaSkinConfigurationLookups.RightColumnSpacing:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(1));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.StagePaddingBottom:
|
||||
case LegacyManiaSkinConfigurationLookups.StagePaddingTop:
|
||||
@@ -146,7 +152,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(width));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
|
||||
|
||||
var colour = getColourForLayout(columnIndex, stage);
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<Color4>(colour));
|
||||
|
||||
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
var holdNote = (DrawableHoldNote)drawableObject;
|
||||
|
||||
AccentColour.BindTo(drawableObject.AccentColour);
|
||||
IsHitting.BindTo(holdNote.IsHitting);
|
||||
IsHitting.BindTo(holdNote.IsHolding);
|
||||
}
|
||||
|
||||
AccentColour.BindValueChanged(onAccentChanged, true);
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
var wrapMode = bodyStyle == LegacyNoteBodyStyle.Stretch ? WrapMode.ClampToEdge : WrapMode.Repeat;
|
||||
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
isHitting.BindTo(holdNote.IsHitting);
|
||||
isHitting.BindTo(holdNote.IsHolding);
|
||||
|
||||
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30)?.With(d =>
|
||||
{
|
||||
|
||||
@@ -98,6 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||
|
||||
if (combo != null)
|
||||
{
|
||||
@@ -112,10 +113,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
spectatorList.Origin = Anchor.BottomLeft;
|
||||
spectatorList.Position = new Vector2(10, -10);
|
||||
}
|
||||
|
||||
if (leaderboard != null)
|
||||
{
|
||||
leaderboard.Anchor = Anchor.CentreLeft;
|
||||
leaderboard.Origin = Anchor.CentreLeft;
|
||||
leaderboard.X = 10;
|
||||
}
|
||||
})
|
||||
{
|
||||
new LegacyManiaComboCounter(),
|
||||
new SpectatorList(),
|
||||
new DrawableGameplayLeaderboard(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private IBindable<ManiaMobileLayout> mobilePlayStyle = null!;
|
||||
|
||||
private float leftColumnSpacing;
|
||||
private float rightColumnSpacing;
|
||||
|
||||
public Column(int index, bool isSpecial)
|
||||
{
|
||||
Index = index;
|
||||
@@ -126,6 +129,14 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private void onSourceChanged()
|
||||
{
|
||||
AccentColour.Value = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, Index)?.Value ?? Color4.Black;
|
||||
|
||||
leftColumnSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.LeftColumnSpacing, Index))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
rightColumnSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.RightColumnSpacing, Index))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -187,8 +198,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
|
||||
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||
{
|
||||
// Extend input coverage to the gaps close to this column.
|
||||
var spacingInflation = new MarginPadding { Left = leftColumnSpacing, Right = rightColumnSpacing };
|
||||
return DrawRectangle.Inflate(spacingInflation).Contains(ToLocalSpace(screenSpacePos));
|
||||
}
|
||||
|
||||
#region Touch Input
|
||||
|
||||
|
||||
@@ -124,14 +124,15 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
for (int i = 0; i < stageDefinition.Columns; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
float spacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1))
|
||||
float leftSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.LeftColumnSpacing, i))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = spacing };
|
||||
}
|
||||
float rightSpacing = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.RightColumnSpacing, i))
|
||||
?.Value ?? Stage.COLUMN_SPACING;
|
||||
|
||||
columns[i].Margin = new MarginPadding { Left = leftSpacing, Right = rightSpacing };
|
||||
|
||||
float? width = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i))
|
||||
|
||||
@@ -60,8 +60,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private readonly BindableDouble configScrollSpeed = new BindableDouble();
|
||||
private readonly Bindable<ManiaMobileLayout> mobileLayout = new Bindable<ManiaMobileLayout>();
|
||||
|
||||
public double TargetTimeRange { get; protected set; }
|
||||
|
||||
private double currentTimeRange;
|
||||
protected double TargetTimeRange;
|
||||
|
||||
// Stores the current speed adjustment active in gameplay.
|
||||
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
|
||||
@@ -109,7 +110,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
|
||||
|
||||
Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
|
||||
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
|
||||
configScrollSpeed.BindValueChanged(speed =>
|
||||
{
|
||||
if (!AllowScrollSpeedAdjustment)
|
||||
return;
|
||||
|
||||
TargetTimeRange = ComputeScrollTime(speed.NewValue);
|
||||
});
|
||||
|
||||
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
receptorGridContent.Add(new ColumnInputReceptor
|
||||
{
|
||||
Action = { BindTarget = column.Action },
|
||||
Spacing = { BindTarget = Spacing },
|
||||
});
|
||||
receptorGridDimensions.Add(new Dimension());
|
||||
|
||||
@@ -122,6 +123,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
public partial class ColumnInputReceptor : CompositeDrawable
|
||||
{
|
||||
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
|
||||
public readonly IBindable<float> Spacing = new BindableFloat();
|
||||
|
||||
private readonly Box highlightOverlay;
|
||||
|
||||
@@ -159,6 +161,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
};
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
// Extend input coverage to the gaps close to this receptor.
|
||||
=> DrawRectangle.Inflate(new Vector2(Spacing.Value / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
updateButton(true);
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
@@ -36,22 +35,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
[Test]
|
||||
public void TestPlayfieldBasedSize()
|
||||
{
|
||||
ModFlashlight mod = new OsuModFlashlight();
|
||||
OsuModFlashlight flashlight;
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = mod,
|
||||
Mods = [flashlight = new OsuModFlashlight(), new OsuModBarrelRoll()],
|
||||
PassCondition = () =>
|
||||
{
|
||||
var flashlightOverlay = Player.DrawableRuleset.Overlays
|
||||
.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>()
|
||||
.First();
|
||||
|
||||
return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize());
|
||||
// the combo check is here because the flashlight radius decreases for the first time at 100 combo
|
||||
// and hardcoding it here eliminates the need to meddle in flashlight internals further by e.g. exposing `GetComboScaleFor()`
|
||||
return flashlightOverlay.GetSize() < flashlight.DefaultFlashlightSize && Player.GameplayState.ScoreProcessor.Combo.Value < 100;
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("adjust playfield scale", () =>
|
||||
Player.DrawableRuleset.Playfield.Scale = new Vector2(.5f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -13,7 +13,6 @@ using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
@@ -22,21 +21,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneOsuModRelax : OsuModTestScene
|
||||
{
|
||||
private readonly HitCircle hitObject;
|
||||
private readonly HitWindows hitWindows = new OsuHitWindows();
|
||||
|
||||
public TestSceneOsuModRelax()
|
||||
{
|
||||
hitWindows.SetDifficulty(9);
|
||||
|
||||
hitObject = new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(100, 100),
|
||||
HitWindows = hitWindows
|
||||
};
|
||||
}
|
||||
|
||||
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModRelaxTestPlayer(CurrentTestData, AllowFail);
|
||||
|
||||
[Test]
|
||||
@@ -46,12 +30,21 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject> { hitObject }
|
||||
Difficulty = { OverallDifficulty = 9 },
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(100, 100),
|
||||
HitWindows = new OsuHitWindows()
|
||||
}
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2()),
|
||||
new OsuReplayFrame(hitObject.StartTime, hitObject.Position),
|
||||
new OsuReplayFrame(100, new Vector2(100)),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||
});
|
||||
@@ -63,13 +56,22 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject> { hitObject }
|
||||
Difficulty = { OverallDifficulty = 9 },
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Position = new Vector2(100, 100),
|
||||
HitWindows = new OsuHitWindows()
|
||||
}
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2(hitObject.X - 22, hitObject.Y - 22)), // must be an edge hit for the cursor to not stay on the object for too long
|
||||
new OsuReplayFrame(hitObject.StartTime - OsuModRelax.RELAX_LENIENCY, new Vector2(hitObject.X - 22, hitObject.Y - 22)),
|
||||
new OsuReplayFrame(hitObject.StartTime, new Vector2(0)),
|
||||
new OsuReplayFrame(0, new Vector2(78, 78)), // must be an edge hit for the cursor to not stay on the object for too long
|
||||
new OsuReplayFrame(1000 - OsuModRelax.RELAX_LENIENCY, new Vector2(78, 78)),
|
||||
new OsuReplayFrame(1000, new Vector2(0)),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||
});
|
||||
|
||||
@@ -49,5 +49,59 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestRewind()
|
||||
{
|
||||
bool seekedBack = false;
|
||||
bool missRecorded = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModStrictTracking(),
|
||||
Autoplay = false,
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath
|
||||
{
|
||||
ControlPoints =
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(0, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2(100, 0)),
|
||||
new OsuReplayFrame(1000, new Vector2(100, 0)),
|
||||
new OsuReplayFrame(1050, new Vector2()),
|
||||
new OsuReplayFrame(1100, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1750, new Vector2(0, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(1751, new Vector2(0, 100)),
|
||||
},
|
||||
PassCondition = () => seekedBack && !missRecorded,
|
||||
});
|
||||
AddStep("subscribe to new judgements", () => Player.ScoreProcessor.NewJudgement += j =>
|
||||
{
|
||||
if (!j.IsHit)
|
||||
missRecorded = true;
|
||||
});
|
||||
AddUntilStep("wait for gameplay completion", () => Player.GameplayState.HasCompleted);
|
||||
AddAssert("no misses", () => missRecorded, () => Is.False);
|
||||
AddStep("seek back", () =>
|
||||
{
|
||||
Player.GameplayClockContainer.Stop();
|
||||
Player.Seek(1040);
|
||||
seekedBack = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,23 +110,23 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new Spinner
|
||||
{
|
||||
StartTime = 0,
|
||||
Duration = 1000,
|
||||
Duration = 3000,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = 2500,
|
||||
StartTime = 4500,
|
||||
RepeatCount = 0,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero),
|
||||
new PathControlPoint(new Vector2(100, 0)),
|
||||
new PathControlPoint(new Vector2(200, 0)),
|
||||
})
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 4500,
|
||||
StartTime = 10000,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneLegacyReplayPlayback : LegacyReplayPlaybackTestScene
|
||||
{
|
||||
protected override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
protected override string? ExportLocation => null;
|
||||
|
||||
private static readonly object[][] no_mod_test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
// Additionally, note that offsets provided in double will be rounded to the nearest integer.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// GREAT hit window is ( -50ms, 50ms)
|
||||
// OK hit window is (-100ms, 100ms)
|
||||
// MEH hit window is (-150ms, 150ms)
|
||||
new object[] { 5f, 48d, HitResult.Great },
|
||||
new object[] { 5f, 49d, HitResult.Great },
|
||||
new object[] { 5f, 50d, HitResult.Ok },
|
||||
new object[] { 5f, 51d, HitResult.Ok },
|
||||
new object[] { 5f, 98d, HitResult.Ok },
|
||||
new object[] { 5f, 99d, HitResult.Ok },
|
||||
new object[] { 5f, 100d, HitResult.Meh },
|
||||
new object[] { 5f, 101d, HitResult.Meh },
|
||||
new object[] { 5f, 148d, HitResult.Meh },
|
||||
new object[] { 5f, 149d, HitResult.Meh },
|
||||
new object[] { 5f, 150d, HitResult.Miss },
|
||||
new object[] { 5f, 151d, HitResult.Miss },
|
||||
|
||||
// OD = 5.7 test cases.
|
||||
// GREAT hit window is ( -45ms, 45ms)
|
||||
// OK hit window is ( -94ms, 94ms)
|
||||
// MEH hit window is (-143ms, 143ms)
|
||||
new object[] { 5.7f, 43d, HitResult.Great },
|
||||
new object[] { 5.7f, 44d, HitResult.Great },
|
||||
new object[] { 5.7f, 45d, HitResult.Ok },
|
||||
new object[] { 5.7f, 46d, HitResult.Ok },
|
||||
new object[] { 5.7f, 92d, HitResult.Ok },
|
||||
new object[] { 5.7f, 93d, HitResult.Ok },
|
||||
new object[] { 5.7f, 94d, HitResult.Meh },
|
||||
new object[] { 5.7f, 95d, HitResult.Meh },
|
||||
new object[] { 5.7f, 141d, HitResult.Meh },
|
||||
new object[] { 5.7f, 142d, HitResult.Meh },
|
||||
new object[] { 5.7f, 143d, HitResult.Miss },
|
||||
new object[] { 5.7f, 144d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] hard_rock_test_cases =
|
||||
{
|
||||
// OD = 5 test cases.
|
||||
// This leads to "effective" OD of 7.
|
||||
// GREAT hit window is ( -38ms, 38ms)
|
||||
// OK hit window is ( -84ms, 84ms)
|
||||
// MEH hit window is (-130ms, 130ms)
|
||||
new object[] { 5f, 36d, HitResult.Great },
|
||||
new object[] { 5f, 37d, HitResult.Great },
|
||||
new object[] { 5f, 38d, HitResult.Ok },
|
||||
new object[] { 5f, 39d, HitResult.Ok },
|
||||
new object[] { 5f, 82d, HitResult.Ok },
|
||||
new object[] { 5f, 83d, HitResult.Ok },
|
||||
new object[] { 5f, 84d, HitResult.Meh },
|
||||
new object[] { 5f, 85d, HitResult.Meh },
|
||||
new object[] { 5f, 128d, HitResult.Meh },
|
||||
new object[] { 5f, 129d, HitResult.Meh },
|
||||
new object[] { 5f, 130d, HitResult.Miss },
|
||||
new object[] { 5f, 131d, HitResult.Miss },
|
||||
|
||||
// OD = 8 test cases.
|
||||
// This would lead to "effective" OD of 11.2,
|
||||
// but the effects are capped to OD 10.
|
||||
// GREAT hit window is ( -20ms, 20ms)
|
||||
// OK hit window is ( -60ms, 60ms)
|
||||
// MEH hit window is (-100ms, 100ms)
|
||||
new object[] { 8f, 18d, HitResult.Great },
|
||||
new object[] { 8f, 19d, HitResult.Great },
|
||||
new object[] { 8f, 20d, HitResult.Ok },
|
||||
new object[] { 8f, 21d, HitResult.Ok },
|
||||
new object[] { 8f, 58d, HitResult.Ok },
|
||||
new object[] { 8f, 59d, HitResult.Ok },
|
||||
new object[] { 8f, 60d, HitResult.Meh },
|
||||
new object[] { 8f, 61d, HitResult.Meh },
|
||||
new object[] { 8f, 98d, HitResult.Meh },
|
||||
new object[] { 8f, 99d, HitResult.Meh },
|
||||
new object[] { 8f, 100d, HitResult.Miss },
|
||||
new object[] { 8f, 101d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private static readonly object[][] easy_test_cases =
|
||||
{
|
||||
// OD = 5 test cases.
|
||||
// This leads to "effective" OD of 2.5.
|
||||
// GREAT hit window is ( -65ms, 65ms)
|
||||
// OK hit window is (-120ms, 120ms)
|
||||
// MEH hit window is (-175ms, 175ms)
|
||||
new object[] { 5f, 63d, HitResult.Great },
|
||||
new object[] { 5f, 64d, HitResult.Great },
|
||||
new object[] { 5f, 65d, HitResult.Ok },
|
||||
new object[] { 5f, 66d, HitResult.Ok },
|
||||
new object[] { 5f, 118d, HitResult.Ok },
|
||||
new object[] { 5f, 119d, HitResult.Ok },
|
||||
new object[] { 5f, 120d, HitResult.Meh },
|
||||
new object[] { 5f, 121d, HitResult.Meh },
|
||||
new object[] { 5f, 173d, HitResult.Meh },
|
||||
new object[] { 5f, 174d, HitResult.Meh },
|
||||
new object[] { 5f, 175d, HitResult.Miss },
|
||||
new object[] { 5f, 176d, HitResult.Miss },
|
||||
};
|
||||
|
||||
private const double hit_circle_time = 100;
|
||||
|
||||
[TestCaseSource(nameof(no_mod_test_cases))]
|
||||
public void TestHitWindowTreatment(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
// required for correct playback in stable
|
||||
new OsuReplayFrame(0, new Vector2(256, -500)),
|
||||
new OsuReplayFrame(0, new Vector2(256, -500)),
|
||||
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"single circle @ OD{overallDifficulty}", beatmap, $@"{hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(hard_rock_test_cases))]
|
||||
public void TestHitWindowTreatmentWithHardRock(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
// required for correct playback in stable
|
||||
new OsuReplayFrame(0, new Vector2(256, -500)),
|
||||
new OsuReplayFrame(0, new Vector2(256, -500)),
|
||||
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new OsuModHardRock()]
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"HR single circle @ OD{overallDifficulty}", beatmap, $@"HR {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(easy_test_cases))]
|
||||
public void TestHitWindowTreatmentWithEasy(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
var beatmap = createBeatmap(overallDifficulty);
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
// required for correct playback in stable
|
||||
new OsuReplayFrame(0, new Vector2(256, -500)),
|
||||
new OsuReplayFrame(0, new Vector2(256, -500)),
|
||||
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2),
|
||||
}
|
||||
};
|
||||
|
||||
var score = new Score
|
||||
{
|
||||
Replay = replay,
|
||||
ScoreInfo = new ScoreInfo
|
||||
{
|
||||
Ruleset = CreateRuleset().RulesetInfo,
|
||||
Mods = [new OsuModEasy()]
|
||||
}
|
||||
};
|
||||
|
||||
RunTest($@"EZ single circle @ OD{overallDifficulty}", beatmap, $@"EZ {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
|
||||
}
|
||||
|
||||
private static OsuBeatmap createBeatmap(float overallDifficulty)
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
||||
var beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = hit_circle_time,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
},
|
||||
ControlPointInfo = cpi,
|
||||
};
|
||||
return beatmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Osu.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
@@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Cached]
|
||||
private OsuRulesetConfigManager config = new OsuRulesetConfigManager(null, new OsuRuleset().RulesetInfo);
|
||||
|
||||
private readonly StopwatchClock clock = new StopwatchClock();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
@@ -35,7 +38,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
new OsuPlayfieldAdjustmentContainer
|
||||
{
|
||||
Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay()),
|
||||
Child = analysisContainer = new TestReplayAnalysisOverlay(fabricateReplay())
|
||||
{
|
||||
Clock = new FramedClock(clock)
|
||||
},
|
||||
},
|
||||
settings = new ReplayAnalysisSettings(config),
|
||||
};
|
||||
@@ -55,11 +61,23 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
settings.ShowAimMarkers.Value = true;
|
||||
settings.ShowCursorPath.Value = true;
|
||||
});
|
||||
AddToggleStep("toggle pause", running =>
|
||||
{
|
||||
if (running)
|
||||
clock.Stop();
|
||||
else
|
||||
clock.Start();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitMarkers()
|
||||
{
|
||||
AddStep("stop at 2000", () =>
|
||||
{
|
||||
clock.Stop();
|
||||
clock.Seek(2000);
|
||||
});
|
||||
AddStep("enable hit markers", () => settings.ShowClickMarkers.Value = true);
|
||||
AddUntilStep("hit markers visible", () => analysisContainer.HitMarkersVisible);
|
||||
AddStep("disable hit markers", () => settings.ShowClickMarkers.Value = false);
|
||||
@@ -69,6 +87,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestAimMarker()
|
||||
{
|
||||
AddStep("stop at 2000", () =>
|
||||
{
|
||||
clock.Stop();
|
||||
clock.Seek(2000);
|
||||
});
|
||||
AddStep("enable aim markers", () => settings.ShowAimMarkers.Value = true);
|
||||
AddUntilStep("aim markers visible", () => analysisContainer.AimMarkersVisible);
|
||||
AddStep("disable aim markers", () => settings.ShowAimMarkers.Value = false);
|
||||
@@ -78,6 +101,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestAimLines()
|
||||
{
|
||||
AddStep("stop at 2000", () =>
|
||||
{
|
||||
clock.Stop();
|
||||
clock.Seek(2000);
|
||||
});
|
||||
AddStep("enable aim lines", () => settings.ShowCursorPath.Value = true);
|
||||
AddUntilStep("aim lines visible", () => analysisContainer.AimLinesVisible);
|
||||
AddStep("disable aim lines", () => settings.ShowCursorPath.Value = false);
|
||||
@@ -87,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
private Replay fabricateReplay()
|
||||
{
|
||||
var frames = new List<ReplayFrame>();
|
||||
var random = new Random();
|
||||
var random = new Random(20250522);
|
||||
int posX = 250;
|
||||
int posY = 250;
|
||||
|
||||
@@ -109,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
frames.Add(new OsuReplayFrame
|
||||
{
|
||||
Time = Time.Current + i * 15,
|
||||
Time = i * 15,
|
||||
Position = new Vector2(posX, posY),
|
||||
Actions = actions.ToList(),
|
||||
});
|
||||
|
||||
@@ -14,26 +14,27 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
protected override IResourceStore<byte[]> RulesetResources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneOsuHitObjectSamples)));
|
||||
|
||||
[TestCase("normal-hitnormal")]
|
||||
[TestCase("hitnormal")]
|
||||
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
|
||||
[TestCase("normal-hitnormal2", "normal-hitnormal")]
|
||||
[TestCase("hitnormal", "hitnormal")]
|
||||
public void TestDefaultCustomSampleFromBeatmap(string beatmapSkinSampleName, string userSkinSampleName)
|
||||
{
|
||||
SetupSkins(expectedSample, expectedSample);
|
||||
SetupSkins(beatmapSkinSampleName, userSkinSampleName);
|
||||
|
||||
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(expectedSample);
|
||||
AssertBeatmapLookup(beatmapSkinSampleName);
|
||||
}
|
||||
|
||||
[TestCase("normal-hitnormal")]
|
||||
[TestCase("hitnormal")]
|
||||
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
|
||||
[TestCase("", "normal-hitnormal")]
|
||||
[TestCase("normal-hitnormal", "normal-hitnormal")]
|
||||
[TestCase("", "hitnormal")]
|
||||
public void TestDefaultCustomSampleFromUserSkinFallback(string beatmapSkinSampleName, string userSkinSampleName)
|
||||
{
|
||||
SetupSkins(string.Empty, expectedSample);
|
||||
SetupSkins(beatmapSkinSampleName, userSkinSampleName);
|
||||
|
||||
CreateTestWithBeatmap("osu-hitobject-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertUserLookup(expectedSample);
|
||||
AssertUserLookup(userSkinSampleName);
|
||||
}
|
||||
|
||||
[TestCase("normal-hitnormal2")]
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneReplayRecording : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 0,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 5000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 10000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 15000,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
|
||||
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
|
||||
|
||||
[Test]
|
||||
public void TestRecording()
|
||||
{
|
||||
seekTo(0);
|
||||
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||
AddStep("press X", () => InputManager.PressKey(Key.X));
|
||||
seekTo(15);
|
||||
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
|
||||
AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton])));
|
||||
|
||||
seekTo(5000);
|
||||
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||
AddStep("press Z", () => InputManager.PressKey(Key.Z));
|
||||
seekTo(5015);
|
||||
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
|
||||
AddAssert("left button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.LeftButton])));
|
||||
|
||||
seekTo(10000);
|
||||
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||
AddStep("press C", () => InputManager.PressKey(Key.C));
|
||||
seekTo(10015);
|
||||
AddStep("release C", () => InputManager.ReleaseKey(Key.C));
|
||||
AddAssert("smoke button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.Smoke])));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPressAndReleaseOnSameFrame()
|
||||
{
|
||||
seekTo(0);
|
||||
AddStep("move cursor to circle", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.Single()));
|
||||
AddStep("press X", () => InputManager.PressKey(Key.X));
|
||||
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
|
||||
AddAssert("right button press recorded to replay", () => Player.Score.Replay.Frames.OfType<OsuReplayFrame>().Any(f => f.Actions.SequenceEqual([OsuAction.RightButton])));
|
||||
}
|
||||
|
||||
private void seekTo(double time)
|
||||
{
|
||||
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneReplayStability : ReplayStabilityTestScene
|
||||
{
|
||||
private static readonly object[][] test_cases =
|
||||
{
|
||||
// With respect to notation,
|
||||
// square brackets `[]` represent *closed* or *inclusive* bounds,
|
||||
// while round brackets `()` represent *open* or *exclusive* bounds.
|
||||
|
||||
// OD = 5 test cases.
|
||||
// GREAT hit window is [ -49.5ms, 49.5ms]
|
||||
// OK hit window is [ -99.5ms, 99.5ms]
|
||||
// MEH hit window is [-149.5ms, 149.5ms]
|
||||
new object[] { 5f, 49d, HitResult.Great },
|
||||
new object[] { 5f, 49.2d, HitResult.Great },
|
||||
new object[] { 5f, 49.7d, HitResult.Ok },
|
||||
new object[] { 5f, 50d, HitResult.Ok },
|
||||
new object[] { 5f, 50.4d, HitResult.Ok },
|
||||
new object[] { 5f, 50.9d, HitResult.Ok },
|
||||
new object[] { 5f, 51d, HitResult.Ok },
|
||||
new object[] { 5f, 99d, HitResult.Ok },
|
||||
new object[] { 5f, 99.2d, HitResult.Ok },
|
||||
new object[] { 5f, 99.7d, HitResult.Meh },
|
||||
new object[] { 5f, 100d, HitResult.Meh },
|
||||
new object[] { 5f, 100.4d, HitResult.Meh },
|
||||
new object[] { 5f, 100.9d, HitResult.Meh },
|
||||
new object[] { 5f, 101d, HitResult.Meh },
|
||||
new object[] { 5f, 149d, HitResult.Meh },
|
||||
new object[] { 5f, 149.2d, HitResult.Meh },
|
||||
new object[] { 5f, 149.7d, HitResult.Miss },
|
||||
new object[] { 5f, 150d, HitResult.Miss },
|
||||
new object[] { 5f, 150.4d, HitResult.Miss },
|
||||
new object[] { 5f, 150.9d, HitResult.Miss },
|
||||
new object[] { 5f, 151d, HitResult.Miss },
|
||||
|
||||
// OD = 5.7 test cases.
|
||||
// GREAT hit window is [ -44.5ms, 44.5ms]
|
||||
// OK hit window is [ -93.5ms, 93.5ms]
|
||||
// MEH hit window is [-142.5ms, 142.5ms]
|
||||
new object[] { 5.7f, 44d, HitResult.Great },
|
||||
new object[] { 5.7f, 44.2d, HitResult.Great },
|
||||
new object[] { 5.7f, 44.8d, HitResult.Ok },
|
||||
new object[] { 5.7f, 45d, HitResult.Ok },
|
||||
new object[] { 5.7f, 45.4d, HitResult.Ok },
|
||||
new object[] { 5.7f, 93d, HitResult.Ok },
|
||||
new object[] { 5.7f, 93.4d, HitResult.Ok },
|
||||
new object[] { 5.7f, 93.9d, HitResult.Meh },
|
||||
new object[] { 5.7f, 94d, HitResult.Meh },
|
||||
new object[] { 5.7f, 94.4d, HitResult.Meh },
|
||||
new object[] { 5.7f, 142d, HitResult.Meh },
|
||||
new object[] { 5.7f, 142.2d, HitResult.Meh },
|
||||
new object[] { 5.7f, 142.7d, HitResult.Miss },
|
||||
new object[] { 5.7f, 143d, HitResult.Miss },
|
||||
new object[] { 5.7f, 143.4d, HitResult.Miss },
|
||||
new object[] { 5.7f, 143.9d, HitResult.Miss },
|
||||
new object[] { 5.7f, 144d, HitResult.Miss },
|
||||
};
|
||||
|
||||
[TestCaseSource(nameof(test_cases))]
|
||||
public void TestHitWindowStability(float overallDifficulty, double hitOffset, HitResult expectedResult)
|
||||
{
|
||||
const double hit_circle_time = 100;
|
||||
|
||||
var beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = hit_circle_time,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
}
|
||||
},
|
||||
Difficulty = new BeatmapDifficulty { OverallDifficulty = overallDifficulty },
|
||||
BeatmapInfo =
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
},
|
||||
};
|
||||
|
||||
var replay = new Replay
|
||||
{
|
||||
Frames =
|
||||
{
|
||||
new OsuReplayFrame(0, OsuPlayfield.BASE_SIZE / 2),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(hit_circle_time + hitOffset + 20, OsuPlayfield.BASE_SIZE / 2),
|
||||
}
|
||||
};
|
||||
|
||||
RunTest(beatmap, replay, [expectedResult]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,6 +484,47 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("no miss judgements recorded", () => judgementResults.All(r => r.Type.IsHit()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sliders are common to by 1/2 or 1/4 beat length in order to place the circle on the next beat.
|
||||
/// This tests a user pressing the next circle in the window between the last tick and the end of the slider (<see cref="SliderEventGenerator.TAIL_LENIENCY"/>).
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestHitNextCircleDuringTailLeniency()
|
||||
{
|
||||
const double bpm = 240;
|
||||
const double beat_length = 60000 / bpm;
|
||||
const double slider_start = time_slider_start;
|
||||
const double slider_end = slider_start + beat_length;
|
||||
const double last_tick_time = slider_end + SliderEventGenerator.TAIL_LENIENCY;
|
||||
const double next_circle_time = slider_end + beat_length / 4;
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame { Position = Vector2.Zero, Actions = { OsuAction.LeftButton }, Time = time_slider_start },
|
||||
new OsuReplayFrame { Position = new Vector2(140, 0), Actions = { OsuAction.RightButton }, Time = last_tick_time + 20 },
|
||||
},
|
||||
[
|
||||
new Slider
|
||||
{
|
||||
StartTime = slider_start,
|
||||
Position = new Vector2(0, 0),
|
||||
TickDistanceMultiplier = 10, // no ticks
|
||||
Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(100, 0),
|
||||
}, 100),
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = next_circle_time,
|
||||
Position = new Vector2(140, 0)
|
||||
}
|
||||
], bpm: bpm);
|
||||
|
||||
AddAssert("all judgements are hit", () => judgementResults.All(j => j.Type.IsHit()));
|
||||
}
|
||||
|
||||
private void assertAllMaxJudgements()
|
||||
{
|
||||
AddAssert("All judgements max", () =>
|
||||
@@ -522,6 +563,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
}, slider_path_length),
|
||||
};
|
||||
|
||||
performTest(frames, [slider], bpm, tickRate);
|
||||
}
|
||||
|
||||
private void performTest(List<ReplayFrame> frames, List<OsuHitObject> objects, double? bpm = null, int? tickRate = null)
|
||||
{
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
@@ -531,7 +577,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects = { slider },
|
||||
HitObjects = objects,
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty
|
||||
|
||||
@@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 99, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 99, slider_end_position, OsuAction.LeftButton),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Ok);
|
||||
@@ -70,8 +70,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 100, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 100, slider_end_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 99, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 99, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.SliderVelocityMultiplier = 2;
|
||||
@@ -91,8 +91,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 149, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.TickDistanceMultiplier = 0.2f;
|
||||
@@ -116,8 +116,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_end_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 149, slider_end_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.SliderVelocityMultiplier = 2;
|
||||
@@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.LINEAR, new[]
|
||||
@@ -195,8 +195,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.LINEAR, new[]
|
||||
@@ -224,8 +224,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
@@ -259,8 +259,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
@@ -289,8 +289,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 149, slider_start_position, OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 149, slider_start_position, OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
@@ -320,8 +320,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(time_slider_start + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 150, slider_start_position - new Vector2(20), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_start + 149, slider_start_position - new Vector2(20), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(time_slider_end + 149, slider_start_position - new Vector2(20), OsuAction.LeftButton),
|
||||
}, s =>
|
||||
{
|
||||
s.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
|
||||
@@ -86,9 +86,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
[Test]
|
||||
public void TestSpinningSamplePitchShift()
|
||||
{
|
||||
PausableSkinnableSound spinSample = null;
|
||||
|
||||
AddStep("Add spinner", () => SetContents(_ => testSingle(5, true, 4000)));
|
||||
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
|
||||
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
|
||||
AddUntilStep("wait for spin sample", () => (spinSample = getSpinningSample()) != null);
|
||||
AddUntilStep("Pitch starts low", () => spinSample.Frequency.Value < 0.8);
|
||||
AddUntilStep("Pitch increases", () => spinSample.Frequency.Value > 0.8);
|
||||
|
||||
PausableSkinnableSound getSpinningSample() =>
|
||||
drawableSpinner.ChildrenOfType<PausableSkinnableSound>().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
|
||||
|
||||
@@ -22,6 +22,7 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
@@ -107,6 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestVibrateWithoutSpinningOnCentreWithDoubleTime()
|
||||
{
|
||||
List<ReplayFrame> frames = new List<ReplayFrame>();
|
||||
|
||||
@@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
@@ -152,6 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
public void TestSpinPerMinuteOnRewind()
|
||||
{
|
||||
double estimatedSpm = 0;
|
||||
|
||||
@@ -476,15 +476,24 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private class TestHitWindows : HitWindows
|
||||
{
|
||||
private static readonly DifficultyRange[] ranges =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 500, 500, 500),
|
||||
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
|
||||
};
|
||||
|
||||
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
|
||||
|
||||
protected override DifficultyRange[] GetRanges() => ranges;
|
||||
public override void SetDifficulty(double difficulty) { }
|
||||
|
||||
public override double WindowFor(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Great:
|
||||
return 500;
|
||||
|
||||
case HitResult.Miss:
|
||||
return early_miss_window;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(result), result, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
@@ -16,26 +17,30 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
int circles = HitObjects.Count(c => c is HitCircle);
|
||||
int sliders = HitObjects.Count(s => s is Slider);
|
||||
int spinners = HitObjects.Count(s => s is Spinner);
|
||||
int sum = Math.Max(1, circles + sliders);
|
||||
|
||||
return new[]
|
||||
{
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = BeatmapsetsStrings.ShowStatsCountCircles,
|
||||
Name = BeatmapStatisticStrings.Circles,
|
||||
Content = circles.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
|
||||
BarDisplayLength = circles / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = BeatmapsetsStrings.ShowStatsCountSliders,
|
||||
Name = BeatmapStatisticStrings.Sliders,
|
||||
Content = sliders.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
|
||||
BarDisplayLength = sliders / (float)sum,
|
||||
},
|
||||
new BeatmapStatistic
|
||||
{
|
||||
Name = @"Spinner Count",
|
||||
Name = BeatmapStatisticStrings.Spinners,
|
||||
Content = spinners.ToString(),
|
||||
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
|
||||
BarDisplayLength = Math.Min(spinners / 10f, 1),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
{
|
||||
public class OsuBeatmapProcessor : BeatmapProcessor
|
||||
{
|
||||
private const int stack_distance = 3;
|
||||
/// <summary>
|
||||
/// The maximum distance between the end of one object and the start of another
|
||||
/// which allows the objects to be stacked on top of another.
|
||||
/// </summary>
|
||||
public const int STACK_DISTANCE = 3;
|
||||
|
||||
public OsuBeatmapProcessor(IBeatmap beatmap)
|
||||
: base(beatmap)
|
||||
@@ -93,8 +97,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
// We are no longer within stacking range of the next object.
|
||||
break;
|
||||
|
||||
if (Vector2Extensions.Distance(stackBaseObject.Position, objectN.Position) < stack_distance
|
||||
|| (stackBaseObject is Slider && Vector2Extensions.Distance(stackBaseObject.EndPosition, objectN.Position) < stack_distance))
|
||||
if (Vector2Extensions.Distance(stackBaseObject.Position, objectN.Position) < STACK_DISTANCE
|
||||
|| (stackBaseObject is Slider && Vector2Extensions.Distance(stackBaseObject.EndPosition, objectN.Position) < STACK_DISTANCE))
|
||||
{
|
||||
stackBaseIndex = n;
|
||||
|
||||
@@ -163,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
* o <- hitCircle has stack of -1
|
||||
* o <- hitCircle has stack of -2
|
||||
*/
|
||||
if (objectN is Slider && Vector2Extensions.Distance(objectN.EndPosition, objectI.Position) < stack_distance)
|
||||
if (objectN is Slider && Vector2Extensions.Distance(objectN.EndPosition, objectI.Position) < STACK_DISTANCE)
|
||||
{
|
||||
int offset = objectI.StackHeight - objectN.StackHeight + 1;
|
||||
|
||||
@@ -171,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
{
|
||||
// For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
|
||||
OsuHitObject objectJ = hitObjects[j];
|
||||
if (Vector2Extensions.Distance(objectN.EndPosition, objectJ.Position) < stack_distance)
|
||||
if (Vector2Extensions.Distance(objectN.EndPosition, objectJ.Position) < STACK_DISTANCE)
|
||||
objectJ.StackHeight -= offset;
|
||||
}
|
||||
|
||||
@@ -180,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
break;
|
||||
}
|
||||
|
||||
if (Vector2Extensions.Distance(objectN.Position, objectI.Position) < stack_distance)
|
||||
if (Vector2Extensions.Distance(objectN.Position, objectI.Position) < STACK_DISTANCE)
|
||||
{
|
||||
// Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out.
|
||||
//NOTE: Sliders with start positions stacking are a special case that is also handled here.
|
||||
@@ -204,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
// We are no longer within stacking range of the previous object.
|
||||
break;
|
||||
|
||||
if (Vector2Extensions.Distance(objectN.EndPosition, objectI.Position) < stack_distance)
|
||||
if (Vector2Extensions.Distance(objectN.EndPosition, objectI.Position) < STACK_DISTANCE)
|
||||
{
|
||||
objectN.StackHeight = objectI.StackHeight + 1;
|
||||
objectI = objectN;
|
||||
@@ -245,12 +249,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
// Effects of this can be seen on https://osu.ppy.sh/beatmapsets/243#osu/1146 at sliders around 86647 ms, where
|
||||
// if we use `EndTime` here it would result in unexpected stacking.
|
||||
|
||||
if (Vector2Extensions.Distance(hitObjects[j].Position, currHitObject.Position) < stack_distance)
|
||||
if (Vector2Extensions.Distance(hitObjects[j].Position, currHitObject.Position) < STACK_DISTANCE)
|
||||
{
|
||||
currHitObject.StackHeight++;
|
||||
startTime = hitObjects[j].StartTime;
|
||||
}
|
||||
else if (Vector2Extensions.Distance(hitObjects[j].Position, position2) < stack_distance)
|
||||
else if (Vector2Extensions.Distance(hitObjects[j].Position, position2) < STACK_DISTANCE)
|
||||
{
|
||||
// Case for sliders - bump notes down and right, rather than up and left.
|
||||
sliderStack++;
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
overallDifficulty = (80 - greatHitWindow) / 6;
|
||||
overallDifficulty = (79.5 - greatHitWindow) / 6;
|
||||
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
|
||||
|
||||
if (osuAttributes.SliderCount > 0)
|
||||
|
||||
@@ -275,6 +275,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
else
|
||||
{
|
||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
|
||||
// do not allow the slider to extend beyond the path's calculated distance.
|
||||
// this can happen in two specific circumstances:
|
||||
// - floating point issues (`minDistance` is just ever so slightly larger than the calculated distance)
|
||||
// - the slider was placed with a higher beat snap active than the current one,
|
||||
// therefore snapping it to the current beat snap distance would mean extrapolating it beyond its actual shape as defined by its control points
|
||||
minDistance = Math.Min(minDistance, HitObject.Path.CalculatedDistance);
|
||||
|
||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
|
||||
proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Checks;
|
||||
|
||||
@@ -16,6 +17,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
// Compose
|
||||
new CheckOffscreenObjects(),
|
||||
new CheckTooShortSpinners(),
|
||||
new CheckConcurrentObjects(),
|
||||
|
||||
// Spread
|
||||
new CheckTimeDistanceEquality(),
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
@@ -16,10 +17,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||
{
|
||||
// If the pair of hit objects in question here could feasibly be on the same stack, do not provide a distance snap value -
|
||||
// they're likely too close to one another for the distance snap value to be useful anyway even if they somehow are not.
|
||||
if (Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position) < OsuBeatmapProcessor.STACK_DISTANCE)
|
||||
return 0;
|
||||
|
||||
var lastObjectWithVelocity = EditorBeatmap.HitObjects.TakeWhile(ho => ho != after).OfType<IHasSliderVelocity>().LastOrDefault();
|
||||
|
||||
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime, lastObjectWithVelocity);
|
||||
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
|
||||
float actualDistance = Vector2.Distance(((OsuHitObject)before).StackedEndPosition, ((OsuHitObject)after).StackedPosition);
|
||||
|
||||
return actualDistance / expectedDistance;
|
||||
}
|
||||
|
||||
@@ -254,11 +254,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
[CanBeNull]
|
||||
public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null)
|
||||
{
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid?.IsLoaded != true)
|
||||
return null;
|
||||
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
|
||||
|
||||
if (pos.X < 0 || pos.X > OsuPlayfield.BASE_SIZE.X || pos.Y < 0 || pos.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||
return null;
|
||||
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Current = new BindableNumber<int>(3)
|
||||
{
|
||||
MinValue = 3,
|
||||
MaxValue = 10,
|
||||
MaxValue = 32,
|
||||
Precision = 1,
|
||||
},
|
||||
Instantaneous = true
|
||||
|
||||
@@ -91,6 +91,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
@@ -105,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
KeyboardStep = 1,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
@@ -119,6 +121,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
Caption = "Stack Leniency",
|
||||
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableFloat(Beatmap.StackLeniency)
|
||||
{
|
||||
Default = 0.7f,
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -36,6 +37,24 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
|
||||
};
|
||||
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate))
|
||||
return string.Empty;
|
||||
|
||||
if (!CircleSize.IsDefault) return format("CS", CircleSize);
|
||||
if (!ApproachRate.IsDefault) return format("AR", ApproachRate);
|
||||
if (!OverallDifficulty.IsDefault) return format("OD", OverallDifficulty);
|
||||
if (!DrainRate.IsDefault) return format("HP", DrainRate);
|
||||
|
||||
return string.Empty;
|
||||
|
||||
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
|
||||
{
|
||||
get
|
||||
|
||||
@@ -2,12 +2,20 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModEasy : ModEasyWithExtraLives
|
||||
{
|
||||
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!";
|
||||
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!";
|
||||
|
||||
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
base.ApplyToDifficulty(difficulty);
|
||||
|
||||
difficulty.OverallDifficulty *= ADJUST_RATIO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
foreach (var obj in beatmap.HitObjects.OfType<OsuHitObject>())
|
||||
{
|
||||
if (obj.NewCombo) { lastNewComboTime = obj.StartTime; }
|
||||
if (obj.NewCombo)
|
||||
{
|
||||
lastNewComboTime = obj.StartTime;
|
||||
}
|
||||
|
||||
applyFadeInAdjustment(obj);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user