mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 02:09:52 +08:00
Compare commits
3922 Commits
2025.118.3
...
2025.904.0
@@ -36,7 +36,7 @@ jobs:
|
||||
generator:
|
||||
name: Run
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 720
|
||||
timeout-minutes: 1440
|
||||
|
||||
outputs:
|
||||
target: ${{ steps.run.outputs.target }}
|
||||
|
||||
@@ -82,8 +82,18 @@ jobs:
|
||||
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
|
||||
|
||||
- name: Test
|
||||
run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
|
||||
shell: pwsh
|
||||
run: >
|
||||
dotnet test
|
||||
osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll
|
||||
osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll
|
||||
osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll
|
||||
osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll
|
||||
osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll
|
||||
osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll
|
||||
Templates/**/*.Tests/bin/Debug/**/*.Tests.dll
|
||||
--logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
|
||||
--
|
||||
NUnit.ConsoleOut=0
|
||||
|
||||
# Attempt to upload results even if test fails.
|
||||
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
|
||||
@@ -114,17 +124,14 @@ jobs:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Install .NET workloads
|
||||
# since windows image 20241113.3.0, not specifying a version here
|
||||
# installs the .NET 7 version of android workload for very unknown reasons.
|
||||
# revisit once we upgrade to .NET 9, it's probably fixed there.
|
||||
run: dotnet workload install android --version (dotnet --version)
|
||||
run: dotnet workload install android
|
||||
|
||||
- name: Compile
|
||||
run: dotnet build -c Debug osu.Android.slnf
|
||||
|
||||
build-only-ios:
|
||||
name: Build only (iOS)
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -136,7 +143,12 @@ jobs:
|
||||
dotnet-version: "8.0.x"
|
||||
|
||||
- name: Install .NET Workloads
|
||||
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
|
||||
run: dotnet workload install ios
|
||||
|
||||
# https://github.com/dotnet/macios/issues/19157
|
||||
# https://github.com/actions/runner-images/issues/12758
|
||||
- name: Use Xcode 16.4
|
||||
run: sudo xcode-select -switch /Applications/Xcode_16.4.app
|
||||
|
||||
- name: Build
|
||||
run: dotnet build -c Debug osu.iOS
|
||||
run: dotnet build -c Debug osu.iOS.slnf
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="osu.Android">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
@@ -18,3 +18,10 @@ M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize(
|
||||
M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
|
||||
M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
|
||||
M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead.
|
||||
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
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
+2
-2
@@ -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 System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -9,7 +10,6 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Pippidon.Objects;
|
||||
using osu.Game.Rulesets.Pippidon.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Pippidon.Beatmaps
|
||||
{
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
|
||||
};
|
||||
}
|
||||
|
||||
private int getLane(HitObject hitObject) => (int)MathHelper.Clamp(
|
||||
private int getLane(HitObject hitObject) => (int)Math.Clamp(
|
||||
(getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1);
|
||||
|
||||
private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X;
|
||||
|
||||
+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.115.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.903.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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 Android.Content.PM;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
public partial class GameplayScreenRotationLocker : Component
|
||||
{
|
||||
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameActivity gameActivity { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ILocalUserPlayInfo localUserPlayInfo)
|
||||
{
|
||||
localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
|
||||
localUserPlaying.BindValueChanged(updateLock, true);
|
||||
}
|
||||
|
||||
private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
|
||||
{
|
||||
gameActivity.RunOnUiThread(() =>
|
||||
{
|
||||
gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,8 @@ namespace osu.Android
|
||||
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
|
||||
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
|
||||
|
||||
public new bool IsTablet { get; private set; }
|
||||
|
||||
private readonly OsuGameAndroid game;
|
||||
|
||||
private bool gameCreated;
|
||||
@@ -89,9 +91,9 @@ namespace osu.Android
|
||||
WindowManager.DefaultDisplay.GetSize(displaySize);
|
||||
#pragma warning restore CA1422
|
||||
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
|
||||
bool isTablet = smallestWidthDp >= 600f;
|
||||
IsTablet = smallestWidthDp >= 600f;
|
||||
|
||||
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
|
||||
RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
|
||||
|
||||
// Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
|
||||
// The assembly files are not available as files either after native AOT.
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Microsoft.Maui.Devices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Android
|
||||
{
|
||||
@@ -18,60 +23,62 @@ namespace osu.Android
|
||||
[Cached]
|
||||
private readonly OsuGameActivity gameActivity;
|
||||
|
||||
private readonly PackageInfo packageInfo;
|
||||
|
||||
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
|
||||
|
||||
public OsuGameAndroid(OsuGameActivity activity)
|
||||
: base(null)
|
||||
{
|
||||
gameActivity = activity;
|
||||
packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
|
||||
}
|
||||
|
||||
public override Version AssemblyVersion
|
||||
public override string Version
|
||||
{
|
||||
get
|
||||
{
|
||||
var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
|
||||
if (!IsDeployedBuild)
|
||||
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
|
||||
|
||||
try
|
||||
{
|
||||
// We store the osu! build number in the "VersionCode" field to better support google play releases.
|
||||
// If we were to use the main build number, it would require a new submission each time (similar to TestFlight).
|
||||
// In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time.
|
||||
//
|
||||
// We also need to be aware that older SDK versions store this as a 32bit int.
|
||||
//
|
||||
// Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060
|
||||
|
||||
// https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
|
||||
string versionName;
|
||||
|
||||
if (OperatingSystem.IsAndroidVersionAtLeast(28))
|
||||
{
|
||||
versionName = packageInfo.LongVersionCode.ToString();
|
||||
// ensure we only read the trailing portion of long (the part we are interested in).
|
||||
versionName = versionName.Substring(versionName.Length - 9);
|
||||
}
|
||||
else
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// this is required else older SDKs will report missing method exception.
|
||||
versionName = packageInfo.VersionCode.ToString();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
// undo play store version garbling (as mentioned above).
|
||||
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return new Version(packageInfo.VersionName.AsNonNull());
|
||||
return packageInfo.VersionName.AsNonNull();
|
||||
}
|
||||
}
|
||||
|
||||
public override Version AssemblyVersion => new Version(packageInfo.VersionName.AsNonNull().Split('-').First());
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
|
||||
UserPlayingState.BindValueChanged(_ => updateOrientation());
|
||||
}
|
||||
|
||||
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
|
||||
{
|
||||
base.ScreenChanged(current, newScreen);
|
||||
|
||||
if (newScreen != null)
|
||||
updateOrientation();
|
||||
}
|
||||
|
||||
private void updateOrientation()
|
||||
{
|
||||
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet);
|
||||
|
||||
switch (orientation)
|
||||
{
|
||||
case MobileUtils.Orientation.Locked:
|
||||
gameActivity.RequestedOrientation = ScreenOrientation.Locked;
|
||||
break;
|
||||
|
||||
case MobileUtils.Orientation.Portrait:
|
||||
gameActivity.RequestedOrientation = ScreenOrientation.Portrait;
|
||||
break;
|
||||
|
||||
case MobileUtils.Orientation.Default:
|
||||
gameActivity.RequestedOrientation = gameActivity.DefaultOrientation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetHost(GameHost host)
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace osu.Desktop
|
||||
};
|
||||
|
||||
client.OnReady += onReady;
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error);
|
||||
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -173,7 +173,7 @@ namespace osu.Desktop
|
||||
new Button
|
||||
{
|
||||
Label = "View beatmap",
|
||||
Url = $@"{api.WebsiteRootUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
|
||||
Url = $@"{api.Endpoints.WebsiteUrl}/beatmaps/{beatmapId}?mode={ruleset.Value.ShortName}"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -112,7 +123,7 @@ namespace osu.Desktop
|
||||
|
||||
public override bool RestartAppWhenExited()
|
||||
{
|
||||
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
|
||||
Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+28
-6
@@ -28,13 +28,15 @@ namespace osu.Desktop
|
||||
|
||||
private static LegacyTcpIpcProvider? legacyIpc;
|
||||
|
||||
private static bool isFirstRun;
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// IMPORTANT DON'T IGNORE: For general sanity, velopack's setup needs to run before anything else.
|
||||
// This has bitten us in the rear before (bricked updater), and although the underlying issue from
|
||||
// last time has been fixed, let's not tempt fate.
|
||||
setupVelopack();
|
||||
setupVelopack(args);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +174,21 @@ namespace osu.Desktop
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void setupVelopack()
|
||||
private static void setupVelopack(string[] args)
|
||||
{
|
||||
// Arguments being present indicate the user is either starting the game in a special (aka tournament) mode,
|
||||
// or is running with pending imports via file association or otherwise.
|
||||
//
|
||||
// In both these scenarios, we'd hope the game does not attempt to update.
|
||||
//
|
||||
// Special consideration for velopack startup arguments, which must be handled during update.
|
||||
// See https://docs.velopack.io/integrating/hooks#command-line-hooks.
|
||||
if (args.Length > 0 && !args[0].StartsWith("--velo", StringComparison.Ordinal))
|
||||
{
|
||||
Logger.Log("Handling arguments, skipping velopack setup.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (OsuGameDesktop.IsPackageManaged)
|
||||
{
|
||||
Logger.Log("Updates are being managed by an external provider. Skipping Velopack setup.");
|
||||
@@ -177,6 +197,8 @@ namespace osu.Desktop
|
||||
|
||||
var app = VelopackApp.Build();
|
||||
|
||||
app.OnFirstRun(_ => isFirstRun = true);
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
configureWindows(app);
|
||||
|
||||
@@ -186,9 +208,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ namespace osu.Desktop.Security
|
||||
|
||||
private partial class ElevatedPrivilegesNotification : SimpleNotification
|
||||
{
|
||||
public override bool IsImportant => true;
|
||||
|
||||
public ElevatedPrivilegesNotification()
|
||||
{
|
||||
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";
|
||||
|
||||
@@ -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,128 @@ 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);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(INotificationOverlay notifications)
|
||||
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
|
||||
{
|
||||
notificationOverlay = notifications;
|
||||
}
|
||||
scheduledBackgroundCheck?.Cancel();
|
||||
|
||||
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;
|
||||
if (isInGameplay)
|
||||
{
|
||||
log("Update check cancelled - user is in gameplay");
|
||||
scheduleNextUpdateCheck();
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Avoid any kind of update checking while gameplay is running.
|
||||
if (isInGameplay)
|
||||
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
|
||||
{
|
||||
scheduleRecheck = true;
|
||||
AllowVersionDowngrade = true
|
||||
});
|
||||
|
||||
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
log("Update check cancelled");
|
||||
scheduleNextUpdateCheck();
|
||||
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 (update == 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;
|
||||
// No update is available.
|
||||
log("No update found");
|
||||
scheduleNextUpdateCheck();
|
||||
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!");
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
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})");
|
||||
log($"Update check failed with error ({e.Message})");
|
||||
|
||||
// we shouldn't crash on a web failure. or any failure for the matter.
|
||||
scheduleNextUpdateCheck();
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (scheduleRecheck)
|
||||
CompletionClickAction = () =>
|
||||
{
|
||||
Scheduler.AddDelayed(() => Task.Run(async () => await checkForUpdateAsync().ConfigureAwait(false)), 60000 * 30);
|
||||
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)
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
Binary file not shown.
@@ -24,9 +24,9 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="9.0.0" />
|
||||
<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>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
@@ -21,8 +22,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
|
||||
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
|
||||
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, []);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
|
||||
}
|
||||
@@ -32,8 +34,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75);
|
||||
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModHalfTime()]);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
|
||||
}
|
||||
@@ -43,8 +46,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
var difficulty = new BeatmapDifficulty();
|
||||
var beatmapInfo = new BeatmapInfo { Difficulty = difficulty };
|
||||
|
||||
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5);
|
||||
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(beatmapInfo, [new CatchModDoubleTime()]);
|
||||
|
||||
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
@@ -23,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new CatchRuleset();
|
||||
|
||||
protected const double TIME_SNAP = 100;
|
||||
|
||||
protected DrawableCatchHitObject LastObject;
|
||||
@@ -71,11 +72,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
|
||||
}
|
||||
|
||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||
protected override void UpdatePlacementTimeAndPosition()
|
||||
{
|
||||
var result = base.SnapForBlueprint(blueprint);
|
||||
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
|
||||
return result;
|
||||
var position = InputManager.CurrentState.Mouse.Position;
|
||||
double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP;
|
||||
CurrentBlueprint.UpdateTimeAndPosition(position, time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.Rulesets.Catch.Mods;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneCatchModMovingFast : ModTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestMovingFast() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new CatchModMovingFast(),
|
||||
PassCondition = () => true
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
|
||||
TickDistanceMultiplier = beatmap.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
|
||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||
}.Yield();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@@ -11,9 +12,11 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Edit.Setup;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
@@ -24,6 +27,7 @@ using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Replays.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
@@ -32,6 +36,7 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
@@ -149,6 +154,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
new CatchModFloatingFruits(),
|
||||
new CatchModMuted(),
|
||||
new CatchModNoScope(),
|
||||
new CatchModMovingFast(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
@@ -228,7 +234,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
new DifficultySection(),
|
||||
new CatchDifficultySection(),
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
@@ -264,9 +270,10 @@ namespace osu.Game.Rulesets.Catch
|
||||
}
|
||||
|
||||
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
|
||||
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate)
|
||||
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||
{
|
||||
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
|
||||
BeatmapDifficulty adjustedDifficulty = base.GetAdjustedDisplayDifficulty(beatmapInfo, mods);
|
||||
double rate = ModUtils.CalculateRateWithMods(mods);
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
|
||||
preempt /= rate;
|
||||
@@ -275,6 +282,33 @@ namespace osu.Game.Rulesets.Catch
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
||||
public override IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForDisplay(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||
{
|
||||
var originalDifficulty = beatmapInfo.Difficulty;
|
||||
var effectiveDifficulty = GetAdjustedDisplayDifficulty(beatmapInfo, mods);
|
||||
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.CircleSize, @"CS", originalDifficulty.CircleSize, effectiveDifficulty.CircleSize, 10)
|
||||
{
|
||||
Description = "Affects the size of fruits.",
|
||||
AdditionalMetrics =
|
||||
[
|
||||
new RulesetBeatmapAttribute.AdditionalMetric("Hit circle radius", (CatchHitObject.OBJECT_RADIUS * LegacyRulesetExtensions.CalculateScaleFromCircleSize(effectiveDifficulty.CircleSize)).ToLocalisableString("0.#"))
|
||||
]
|
||||
};
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.ApproachRate, @"AR", originalDifficulty.ApproachRate, effectiveDifficulty.ApproachRate, 10)
|
||||
{
|
||||
Description = "Affects how early fruits fade in on the screen.",
|
||||
AdditionalMetrics =
|
||||
[
|
||||
new RulesetBeatmapAttribute.AdditionalMetric("Fade-in time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRange(effectiveDifficulty.ApproachRate, CatchHitObject.PREEMPT_RANGE):#,0.##} ms"))
|
||||
]
|
||||
};
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, effectiveDifficulty.DrainRate, 10)
|
||||
{
|
||||
Description = "Affects the harshness of health drain and the health penalties for missing."
|
||||
};
|
||||
}
|
||||
|
||||
public override bool EditorShowScrollSpeed => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
@@ -10,15 +9,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("approach_rate")]
|
||||
public double ApproachRate { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
@@ -26,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
||||
yield return (ATTRIB_ID_AIM, StarRating);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -34,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_AIM];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
private float halfCatcherWidth;
|
||||
|
||||
public override int Version => 20220701;
|
||||
public override int Version => 20250306;
|
||||
|
||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -35,14 +36,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new CatchDifficultyAttributes { Mods = mods };
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||
{
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
|
||||
StarRating = Math.Sqrt(skills.OfType<Movement>().Single().DifficultyValue()) * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
@@ -50,7 +53,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
|
||||
|
||||
double approachRate = catchAttributes.ApproachRate;
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
double clockRate = track.Rate;
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
double approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0;
|
||||
|
||||
double approachRateFactor = 1.0;
|
||||
if (approachRate > 9.0)
|
||||
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
|
||||
|
||||
@@ -26,7 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
private float? lastPlayerPosition;
|
||||
private float lastDistanceMoved;
|
||||
private float lastExactDistanceMoved;
|
||||
private double lastStrainTime;
|
||||
private bool isInBuzzSection;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier applied to the player's catcher.
|
||||
@@ -59,6 +61,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
|
||||
@@ -92,12 +97,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
playerPosition = catchCurrent.NormalizedPosition;
|
||||
}
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
|
||||
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
|
||||
{
|
||||
if (isInBuzzSection)
|
||||
distanceAddition = 0;
|
||||
else
|
||||
isInBuzzSection = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isInBuzzSection = false;
|
||||
}
|
||||
|
||||
lastPlayerPosition = playerPosition;
|
||||
lastDistanceMoved = distanceMoved;
|
||||
lastStrainTime = catchCurrent.StrainTime;
|
||||
lastExactDistanceMoved = exactDistanceMoved;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
@@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
|
||||
if (!(result.Time is double time)) return;
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
|
||||
if (!(result.Time is double time)) return result;
|
||||
|
||||
switch (PlacementActive)
|
||||
{
|
||||
@@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
|
||||
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
|
||||
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||
public abstract partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||
where THitObject : CatchHitObject, new()
|
||||
{
|
||||
protected new THitObject HitObject => (THitObject)base.HitObject;
|
||||
@@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; } = null!;
|
||||
|
||||
public CatchPlacementBlueprint()
|
||||
[Resolved]
|
||||
protected CatchHitObjectComposer? Composer { get; private set; }
|
||||
|
||||
protected CatchPlacementBlueprint()
|
||||
: base(new THitObject())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
base.UpdateHitObjectFromPath(hitObject);
|
||||
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLengthForPlacement)
|
||||
EditorBeatmap?.Remove(hitObject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
@@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
base.UpdateTimeAndPosition(result);
|
||||
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
|
||||
|
||||
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
|
||||
? distanceSnapResult
|
||||
: gridSnapResult;
|
||||
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
|
||||
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
|
||||
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
|
||||
|
||||
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
|
||||
? distanceSnapResult
|
||||
: gridSnapResult;
|
||||
|
||||
switch (PlacementActive)
|
||||
{
|
||||
case PlacementState.Waiting:
|
||||
@@ -99,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Make sure the up-to-date position is used for outlines.
|
||||
@@ -113,6 +121,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
ApplyDefaultsToHitObject();
|
||||
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
|
||||
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
|
||||
return result;
|
||||
}
|
||||
|
||||
private double positionToTime(float relativeYPosition)
|
||||
|
||||
@@ -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,14 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
// Compose
|
||||
new CheckBananaShowerGap(),
|
||||
new CheckConcurrentObjects(),
|
||||
|
||||
// Spread
|
||||
new CheckCatchLowestDiffDrainTime(),
|
||||
|
||||
// Settings
|
||||
new CheckCatchAbnormalDifficultySettings(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
// 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 osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public partial class CatchBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer;
|
||||
|
||||
public CatchBlueprintContainer(CatchHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
@@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
}
|
||||
|
||||
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
|
||||
|
||||
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
|
||||
{
|
||||
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||
|
||||
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition);
|
||||
gridSnapResult.ScreenSpacePosition.X = movePosition.X;
|
||||
var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
|
||||
|
||||
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
|
||||
? distanceSnapResult
|
||||
: gridSnapResult;
|
||||
|
||||
var referenceBlueprint = blueprints.First().blueprint;
|
||||
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||
if (moved)
|
||||
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
|
||||
return moved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
//
|
||||
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
||||
|
||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
|
||||
|
||||
float previousEndX = (before as JuiceStream)?.EndX ?? ((CatchHitObject)before).EffectiveX;
|
||||
float actualDistance = Math.Abs(previousEndX - ((CatchHitObject)after).EffectiveX);
|
||||
|
||||
@@ -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;
|
||||
@@ -23,9 +24,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
[Cached]
|
||||
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private const float distance_snap_radius = 50;
|
||||
public const float DISTANCE_SNAP_RADIUS = 50;
|
||||
|
||||
private CatchDistanceSnapGrid distanceSnapGrid = null!;
|
||||
|
||||
@@ -135,22 +137,12 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
DistanceSnapProvider.HandleToggleViaKey(key);
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult)
|
||||
return snapResult;
|
||||
|
||||
result.ScreenSpacePosition.X = screenSpacePosition.X;
|
||||
|
||||
if (snapType.HasFlag(SnapType.RelativeGrids))
|
||||
{
|
||||
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
|
||||
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
|
||||
{
|
||||
result = snapResult;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return null;
|
||||
}
|
||||
|
||||
private PalpableCatchHitObject? getLastSnappableHitObject(double time)
|
||||
@@ -228,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
using Direction = osu.Framework.Graphics.Direction;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
@@ -38,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
return true;
|
||||
}
|
||||
|
||||
moveSelection(deltaX);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void moveSelection(float deltaX)
|
||||
{
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
if (!(h is CatchHitObject catchObject)) return;
|
||||
@@ -48,7 +57,60 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
foreach (var nested in catchObject.NestedHitObjects.OfType<CatchHitObject>())
|
||||
nested.OriginalX += deltaX;
|
||||
});
|
||||
}
|
||||
|
||||
private bool nudgeMovementActive;
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
// Until the keys below are global actions, this will prevent conflicts with "seek between sample points"
|
||||
// which has a default of ctrl+shift+arrows.
|
||||
if (e.ShiftPressed)
|
||||
return false;
|
||||
|
||||
if (e.ControlPressed)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Left:
|
||||
return nudgeSelection(-1);
|
||||
|
||||
case Key.Right:
|
||||
return nudgeSelection(1);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
base.OnKeyUp(e);
|
||||
|
||||
if (nudgeMovementActive && !e.ControlPressed)
|
||||
{
|
||||
EditorBeatmap.EndChange();
|
||||
nudgeMovementActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move the current selection spatially by the specified delta, in gamefield coordinates (ie. the same coordinates as the blueprints).
|
||||
/// </summary>
|
||||
private bool nudgeSelection(float deltaX)
|
||||
{
|
||||
if (!nudgeMovementActive)
|
||||
{
|
||||
nudgeMovementActive = true;
|
||||
EditorBeatmap.BeginChange();
|
||||
}
|
||||
|
||||
var firstBlueprint = SelectedBlueprints.FirstOrDefault();
|
||||
|
||||
if (firstBlueprint == null)
|
||||
return false;
|
||||
|
||||
moveSelection(deltaX);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
var hitObjects = context.CurrentDifficulty.Playable.HitObjects;
|
||||
(int expectedStartDelta, int expectedEndDelta) = spinner_delta_threshold[context.InterpretedDifficulty];
|
||||
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
|
||||
public override IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var diff = context.Beatmap.Difficulty;
|
||||
var diff = context.CurrentDifficulty.Playable.Difficulty;
|
||||
Issue? issue;
|
||||
|
||||
if (HasMoreThanOneDecimalPlace("Approach rate", diff.ApproachRate, out issue))
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
{
|
||||
public class CheckCatchLowestDiffDrainTime : CheckLowestDiffDrainTime
|
||||
{
|
||||
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
|
||||
{
|
||||
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21catch#general
|
||||
yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Platter");
|
||||
yield return (DifficultyRating.Insane, new TimeSpan(0, 3, 15).TotalMilliseconds, "Rain");
|
||||
yield return (DifficultyRating.Expert, new TimeSpan(0, 4, 0).TotalMilliseconds, "Overdose");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Setup
|
||||
{
|
||||
public partial class CatchDifficultySection : SetupSection
|
||||
{
|
||||
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
circleSizeSlider = new FormSliderBar<float>
|
||||
{
|
||||
Caption = BeatmapsetsStrings.ShowStatsCs,
|
||||
HintText = EditorSetupStrings.CircleSizeDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
healthDrainSlider = new FormSliderBar<float>
|
||||
{
|
||||
Caption = BeatmapsetsStrings.ShowStatsDrain,
|
||||
HintText = EditorSetupStrings.DrainRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
approachRateSlider = new FormSliderBar<float>
|
||||
{
|
||||
Caption = BeatmapsetsStrings.ShowStatsAr,
|
||||
HintText = EditorSetupStrings.ApproachRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
baseVelocitySlider = new FormSliderBar<double>
|
||||
{
|
||||
Caption = EditorSetupStrings.BaseVelocity,
|
||||
HintText = EditorSetupStrings.BaseVelocityDescription,
|
||||
KeyboardStep = 0.1f,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
MinValue = 0.4,
|
||||
MaxValue = 3.6,
|
||||
Precision = 0.01f,
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
tickRateSlider = new FormSliderBar<double>
|
||||
{
|
||||
Caption = EditorSetupStrings.TickRate,
|
||||
HintText = EditorSetupStrings.TickRateDescription,
|
||||
KeyboardStep = 1,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 1,
|
||||
MaxValue = 4,
|
||||
Precision = 1,
|
||||
},
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var item in Children.OfType<FormSliderBar<float>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
|
||||
foreach (var item in Children.OfType<FormSliderBar<double>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
}
|
||||
|
||||
private void updateValues()
|
||||
{
|
||||
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
Beatmap.SaveState();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// 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.Rulesets.Catch.Replays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -10,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModAutoplay : ModAutoplay
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
|
||||
|
||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// 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.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
@@ -11,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModCinema : ModCinema<CatchHitObject>
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
|
||||
|
||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
|
||||
}
|
||||
|
||||
@@ -1,10 +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 System.Linq;
|
||||
using System.Collections.Generic;
|
||||
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;
|
||||
|
||||
@@ -35,21 +37,39 @@ 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 SettingDescription
|
||||
public override string ExtendedIconInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
|
||||
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
|
||||
string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
|
||||
if (!IsExactlyOneSettingChanged(CircleSize, ApproachRate, OverallDifficulty, DrainRate))
|
||||
return string.Empty;
|
||||
|
||||
return string.Join(", ", new[]
|
||||
{
|
||||
circleSize,
|
||||
base.SettingDescription,
|
||||
approachRate,
|
||||
spicyPatterns,
|
||||
}.Where(s => !string.IsNullOrEmpty(s)));
|
||||
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
|
||||
{
|
||||
if (!CircleSize.IsDefault)
|
||||
yield return ("Circle size", $"{CircleSize.Value:N1}");
|
||||
|
||||
foreach (var setting in base.SettingDescription)
|
||||
yield return setting;
|
||||
|
||||
if (!ApproachRate.IsDefault)
|
||||
yield return ("Approach rate", $"{ApproachRate.Value:N1}");
|
||||
|
||||
if (!HardRockOffsets.IsDefault)
|
||||
yield return ("Spicy patterns", "On");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
|
||||
public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
|
||||
|
||||
public override float DefaultFlashlightSize => 325;
|
||||
public override float DefaultFlashlightSize => 203.125f;
|
||||
|
||||
protected override Flashlight CreateFlashlight() => new CatchFlashlight(this, playfield);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
public override string Acronym => "FF";
|
||||
public override LocalisableString Description => "The fruits are... floating?";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => FontAwesome.Solid.Cloud;
|
||||
public override IconUsage? Icon => OsuIcon.ModFloatingFruits;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public partial class CatchModMovingFast : Mod, IApplicableToDrawableRuleset<CatchHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Name => "Moving Fast";
|
||||
public override string Acronym => "MF";
|
||||
public override LocalisableString Description => "Dashing by default, slow down!";
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon => OsuIcon.ModMovingFast;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
|
||||
|
||||
private DrawableCatchRuleset drawableRuleset = null!;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
this.drawableRuleset = (DrawableCatchRuleset)drawableRuleset;
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
if (!drawableRuleset.HasReplayLoaded.Value)
|
||||
{
|
||||
var catchPlayfield = (CatchPlayfield)drawableRuleset.Playfield;
|
||||
catchPlayfield.Catcher.Dashing = true;
|
||||
catchPlayfield.CatcherArea.Add(new InvertDashInputHelper(catchPlayfield.CatcherArea));
|
||||
}
|
||||
}
|
||||
|
||||
private partial class InvertDashInputHelper : Drawable, IKeyBindingHandler<CatchAction>
|
||||
{
|
||||
private readonly CatcherArea catcherArea;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
public InvertDashInputHelper(CatcherArea catcherArea)
|
||||
{
|
||||
this.catcherArea = catcherArea;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<CatchAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case CatchAction.MoveLeft or CatchAction.MoveRight:
|
||||
break;
|
||||
|
||||
case CatchAction.Dash:
|
||||
catcherArea.Catcher.Dashing = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<CatchAction> e)
|
||||
{
|
||||
if (e.Action == CatchAction.Dash)
|
||||
catcherArea.Catcher.Dashing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@@ -19,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public override LocalisableString Description => @"Use the mouse to control the catcher.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(CatchModMovingFast) }).ToArray();
|
||||
|
||||
private DrawableCatchRuleset drawableRuleset = null!;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
|
||||
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_RANGE);
|
||||
|
||||
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
|
||||
}
|
||||
@@ -203,6 +203,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
/// </summary>
|
||||
public const double PREEMPT_MAX = 1800;
|
||||
|
||||
public static readonly DifficultyRange PREEMPT_RANGE = new DifficultyRange(PREEMPT_MAX, PREEMPT_MID, PREEMPT_MIN);
|
||||
|
||||
/// <summary>
|
||||
/// The Y position of the hit object is not used in the normal osu!catch gameplay.
|
||||
/// It is preserved to maximize the backward compatibility with the legacy editor, in which the mappers use the Y position to organize the patterns.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||
|
||||
double duration = ObjectState.HitObject.StartTime - ObjectState.DisplayStartTime;
|
||||
|
||||
fadeContent.Alpha = MathHelper.Clamp(
|
||||
fadeContent.Alpha = Math.Clamp(
|
||||
Interpolation.ValueAt(
|
||||
Time.Current, 1f, 0f,
|
||||
ObjectState.DisplayStartTime + duration * lens_flare_start,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@@ -47,6 +48,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
|
||||
var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();
|
||||
|
||||
if (keyCounter != null)
|
||||
{
|
||||
@@ -55,11 +58,27 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
keyCounter.Origin = Anchor.TopRight;
|
||||
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||
}
|
||||
|
||||
if (spectatorList != null)
|
||||
{
|
||||
spectatorList.Anchor = Anchor.BottomLeft;
|
||||
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,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 System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
|
||||
private readonly Container scaleContainer;
|
||||
|
||||
public CatchPlayfieldAdjustmentContainer()
|
||||
{
|
||||
const float base_game_width = 1024f;
|
||||
@@ -26,30 +30,49 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChild = new Container
|
||||
InternalChild = scaleContainer = new Container
|
||||
{
|
||||
// This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits).
|
||||
// Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off.
|
||||
Name = "Visible area",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = base_game_height + extra_bottom_space,
|
||||
Y = extra_bottom_space / 2,
|
||||
Masking = true,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
{
|
||||
Name = "Playable area",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3),
|
||||
Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust,
|
||||
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
// This container limits vertical visibility of the playfield to ensure fairness between wide and tall resolutions (i.e. tall resolutions should not see more fruits).
|
||||
// Note that the container still extends across the screen horizontally, so that hit explosions at the sides of the playfield do not get cut off.
|
||||
Name = "Visible area",
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = base_game_height + extra_bottom_space,
|
||||
Y = extra_bottom_space / 2,
|
||||
Masking = true,
|
||||
Child = new Container
|
||||
{
|
||||
Name = "Playable area",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
Y = base_game_height * ((1 - playfield_size_adjust) / 4 * 3),
|
||||
Size = new Vector2(base_game_width, base_game_height) * playfield_size_adjust,
|
||||
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuGame? osuGame)
|
||||
{
|
||||
if (osuGame != null)
|
||||
{
|
||||
// on mobile platforms where the base aspect ratio is wider, the catch playfield
|
||||
// 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(Math.Min(1, base_aspect_ratio / aspectRatio));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Container"/> which scales its content relative to a target width.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +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.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)
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHoldNotesAlmostConcurrentOnSameColumn()
|
||||
{
|
||||
assertAlmostConcurrentSame(new List<HitObject>
|
||||
{
|
||||
createHoldNote(startTime: 100, endTime: 400.75d, column: 1),
|
||||
createHoldNote(startTime: 408, 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.IssueTemplateConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains("s are concurrent here")));
|
||||
}
|
||||
|
||||
private void assertAlmostConcurrentSame(List<HitObject> hitobjects)
|
||||
{
|
||||
var issues = check.Run(getContext(hitobjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateAlmostConcurrent));
|
||||
Assert.That(issues.All(issue => issue.ToString().Contains("s are less than 10ms apart")));
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
@@ -23,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public abstract partial class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
|
||||
private readonly Column column;
|
||||
|
||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||
@@ -47,12 +48,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
});
|
||||
}
|
||||
|
||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||
protected override void UpdatePlacementTimeAndPosition()
|
||||
{
|
||||
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||
var pos = column.ScreenSpacePositionAtTime(time);
|
||||
|
||||
return new SnapResult(pos, time, column);
|
||||
CurrentBlueprint.UpdateTimeAndPosition(pos, time);
|
||||
}
|
||||
|
||||
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
@@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
@@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
set => InternalChild = value;
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
keyCount.Current.Value = 8;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("refuse", () => InputManager.Key(Key.Number2));
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
keyCount.Current.Value = 8;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
|
||||
AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
[TestCase("convert-samples")]
|
||||
[TestCase("mania-samples")]
|
||||
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
|
||||
[TestCase("slider-convert-samples")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
@@ -32,6 +33,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
StartTime = hitObject.StartTime,
|
||||
EndTime = hitObject.GetEndTime(),
|
||||
Column = ((ManiaHitObject)hitObject).Column,
|
||||
PlaySlidingSamples = hitObject is HoldNote holdNote && holdNote.PlaySlidingSamples,
|
||||
Samples = getSampleNames(hitObject.Samples),
|
||||
NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples)
|
||||
};
|
||||
@@ -57,12 +59,14 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
public double StartTime;
|
||||
public double EndTime;
|
||||
public int Column;
|
||||
public bool PlaySlidingSamples;
|
||||
public IList<string> Samples;
|
||||
public IList<IList<string>> NodeSamples;
|
||||
|
||||
public bool Equals(SampleConvertValue other)
|
||||
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
|
||||
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
|
||||
&& PlaySlidingSamples == other.PlaySlidingSamples
|
||||
&& samplesEqual(Samples, other.Samples)
|
||||
&& nodeSamplesEqual(NodeSamples, other.NodeSamples);
|
||||
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
// 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 TestKeysFilterIntersection()
|
||||
{
|
||||
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 TestInvalidKeysFilters()
|
||||
{
|
||||
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"));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestLnsEqual()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
var filterCriteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
};
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
|
||||
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 0,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
|
||||
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100");
|
||||
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 100
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1");
|
||||
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1");
|
||||
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 1000,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestLnsGreaterOrEqual()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
var filterCriteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
};
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
|
||||
BeatmapInfo beatmapInfo1 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 0,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
|
||||
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 0
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100");
|
||||
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 100
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1");
|
||||
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1");
|
||||
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
TotalObjectCount = 1000,
|
||||
EndTimeObjectCount = 1
|
||||
};
|
||||
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestLnsNotManiaRuleset()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
var filterCriteria = new FilterCriteria
|
||||
{
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
};
|
||||
|
||||
criteria.TryParseCustomKeywordCriteria("lns", Operator.LessOrEqual, "100");
|
||||
BeatmapInfo beatmapInfo = new BeatmapInfo
|
||||
{
|
||||
TotalObjectCount = 100,
|
||||
EndTimeObjectCount = 50
|
||||
};
|
||||
Assert.False(criteria.Matches(beatmapInfo, filterCriteria));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void TestInvalidLnsFilters()
|
||||
{
|
||||
var criteria = new ManiaFilterCriteria();
|
||||
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text"));
|
||||
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))]
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
}
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
+2
@@ -5,6 +5,7 @@
|
||||
"StartTime": 1000.0,
|
||||
"EndTime": 2750.0,
|
||||
"Column": 1,
|
||||
"PlaySlidingSamples": true,
|
||||
"NodeSamples": [
|
||||
["Gameplay/normal-hitnormal"],
|
||||
["Gameplay/soft-hitnormal"],
|
||||
@@ -15,6 +16,7 @@
|
||||
"StartTime": 1875.0,
|
||||
"EndTime": 2750.0,
|
||||
"Column": 0,
|
||||
"PlaySlidingSamples": true,
|
||||
"NodeSamples": [
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/drum-hitnormal"]
|
||||
|
||||
+2
@@ -5,6 +5,7 @@
|
||||
"StartTime": 500.0,
|
||||
"EndTime": 1500.0,
|
||||
"Column": 0,
|
||||
"PlaySlidingSamples": false,
|
||||
"NodeSamples": [
|
||||
["Gameplay/normal-hitnormal"],
|
||||
[]
|
||||
@@ -17,6 +18,7 @@
|
||||
"StartTime": 2000.0,
|
||||
"EndTime": 3000.0,
|
||||
"Column": 2,
|
||||
"PlaySlidingSamples": false,
|
||||
"NodeSamples": [
|
||||
["Gameplay/drum-hitnormal"],
|
||||
[]
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 500.0,
|
||||
"Objects": [{
|
||||
"StartTime": 500.0,
|
||||
"EndTime": 2500,
|
||||
"Column": 2,
|
||||
"PlaySlidingSamples": true,
|
||||
"NodeSamples": [
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal"]
|
||||
],
|
||||
"Samples": ["Gameplay/soft-hitnormal"]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
osu file format v5
|
||||
|
||||
[General]
|
||||
StackLeniency: 0.7
|
||||
Mode: 3
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:2
|
||||
CircleSize:5
|
||||
OverallDifficulty:2
|
||||
SliderMultiplier:1
|
||||
SliderTickRate:2
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
//Break Periods
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Failing)
|
||||
//Storyboard Layer 2 (Passing)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
//Background Colour Transformations
|
||||
3,100,163,162,255
|
||||
|
||||
[TimingPoints]
|
||||
355,476.190476190476,4,2,1,60,1,0
|
||||
|
||||
[HitObjects]
|
||||
256,352,500,2,0,L|256:208,3,140
|
||||
@@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Child = new ColumnHitObjectArea(new HitObjectContainer())
|
||||
Child = new ColumnHitObjectArea
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new HitObjectContainer(),
|
||||
}
|
||||
},
|
||||
new ColumnTestContainer(1, ManiaAction.Key2)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Child = new ColumnHitObjectArea(new HitObjectContainer())
|
||||
Child = new ColumnHitObjectArea
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new HitObjectContainer(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// 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.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneManiaTouchInput : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[SetUp]
|
||||
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
|
||||
|
||||
[Test]
|
||||
public void TestTouchInput()
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int index = i;
|
||||
|
||||
AddStep($"touch column {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(index).Action.Value));
|
||||
|
||||
AddStep($"release column {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(index).Action.Value));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOneColumnMultipleTouches()
|
||||
{
|
||||
AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(0).Action.Value));
|
||||
|
||||
AddStep("touch another finger", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action still pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(0).Action.Value));
|
||||
|
||||
AddStep("release first finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action still pressed",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getColumn(0).Action.Value));
|
||||
|
||||
AddStep("release second finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action released",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => 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
|
||||
|
||||
[Test]
|
||||
public void TestTouchAreaNotInitiallyVisible()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPressReceptors()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int index = i;
|
||||
|
||||
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(index).Action.Value));
|
||||
|
||||
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestColumnsNotTouchableWithTouchControls()
|
||||
{
|
||||
AddStep("enable touch controls", () => toggleTouchControls(true));
|
||||
|
||||
AddStep("touch receptor 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(0).Action.Value));
|
||||
|
||||
AddStep("release receptor 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(0).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f))));
|
||||
|
||||
AddAssert("action not sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Not.Contain(getColumn(0).Action.Value));
|
||||
|
||||
AddStep("release column 0", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre + new Vector2(0f, -50f))));
|
||||
|
||||
AddAssert("action not sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => 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)
|
||||
{
|
||||
var maniaConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(CreatePlayerRuleset())!;
|
||||
maniaConfig.SetValue(ManiaRulesetSetting.TouchOverlay, enabled);
|
||||
}
|
||||
|
||||
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
|
||||
|
||||
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
|
||||
|
||||
private Column getColumn(int index) => this.ChildrenOfType<Column>().ElementAt(index);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
public partial class TestSceneManiaTouchInputArea : PlayerTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestTouchAreaNotInitiallyVisible()
|
||||
{
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPressReceptors()
|
||||
{
|
||||
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int index = i;
|
||||
|
||||
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("action sent",
|
||||
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
|
||||
() => Does.Contain(getReceptor(index).Action.Value));
|
||||
|
||||
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
|
||||
|
||||
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
|
||||
}
|
||||
}
|
||||
|
||||
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
|
||||
|
||||
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
|
||||
}
|
||||
};
|
||||
|
||||
drawableRuleset.AllowBackwardsSeeks = true;
|
||||
});
|
||||
AddStep("retrieve config bindable", () =>
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyList<Mod>? mods = null)
|
||||
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty, IReadOnlyCollection<Mod>? mods = null)
|
||||
{
|
||||
var converter = new ManiaBeatmapConverter(null, difficulty, new ManiaRuleset());
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@@ -30,12 +32,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
if (HitObject is IHasDuration endTimeData)
|
||||
{
|
||||
// despite the beatmap originally being made for mania, if the object is parsed as a slider rather than a hold, sliding samples should still be played.
|
||||
// this is seemingly only possible to achieve by modifying the .osu file directly, but online beatmaps that do that exist
|
||||
// (see second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407)
|
||||
bool playSlidingSamples = (HitObject is IHasLegacyHitObjectType hasType && hasType.LegacyType == LegacyHitObjectType.Slider) || HitObject is IHasPath;
|
||||
|
||||
pattern.Add(new HoldNote
|
||||
{
|
||||
StartTime = HitObject.StartTime,
|
||||
Duration = endTimeData.Duration,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
PlaySlidingSamples = playSlidingSamples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -521,6 +521,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Duration = endTime - startTime,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
PlaySlidingSamples = true,
|
||||
NodeSamples = nodeSamplesAt(startTime)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -15,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
public ManiaRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
|
||||
: base(settings, ruleset, variant)
|
||||
{
|
||||
Migrate();
|
||||
}
|
||||
|
||||
protected override void InitialiseDefaults()
|
||||
@@ -24,17 +24,21 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
SetDefault(ManiaRulesetSetting.ScrollSpeed, 8.0, 1.0, 40.0, 0.1);
|
||||
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
|
||||
SetDefault(ManiaRulesetSetting.MobileLayout, ManiaMobileLayout.Portrait);
|
||||
SetDefault(ManiaRulesetSetting.TouchOverlay, false);
|
||||
}
|
||||
|
||||
#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);
|
||||
public void Migrate()
|
||||
{
|
||||
var mobileLayout = GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout);
|
||||
|
||||
if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (mobileLayout.Value == ManiaMobileLayout.LandscapeWithOverlay)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
SetValue(ManiaRulesetSetting.ScrollSpeed, Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
|
||||
SetValue<double?>(ManiaRulesetSetting.ScrollTime, null);
|
||||
mobileLayout.Value = ManiaMobileLayout.Landscape;
|
||||
SetValue(ManiaRulesetSetting.TouchOverlay, true);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||
@@ -51,10 +55,10 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
|
||||
public enum ManiaRulesetSetting
|
||||
{
|
||||
[Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30
|
||||
ScrollTime,
|
||||
ScrollSpeed,
|
||||
ScrollDirection,
|
||||
TimingBasedNoteColouring
|
||||
TimingBasedNoteColouring,
|
||||
MobileLayout,
|
||||
TouchOverlay,
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user