1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-14 06:42:34 +08:00

Compare commits

...

1524 Commits

1367 changed files with 58563 additions and 25094 deletions
+9 -12
View File
@@ -3,28 +3,25 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2023.3.3",
"version": "2025.2.3",
"commands": [
"jb"
]
},
"nvika": {
"version": "4.0.0",
"commands": [
"nvika"
]
],
"rollForward": false
},
"codefilesanity": {
"version": "0.0.37",
"commands": [
"CodeFileSanity"
]
],
"rollForward": false
},
"ppy.localisationanalyser.tools": {
"version": "2024.802.0",
"version": "2025.1208.0",
"commands": [
"localisation"
]
],
"rollForward": false
}
}
}
}
+5
View File
@@ -19,6 +19,11 @@ indent_style = space
indent_size = 4
trim_trailing_whitespace = true
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130051/Cannot-resolve-symbol-inspections-incorrectly-firing-for-xmldoc-protected-member-references
resharper_c_sharp_warnings_cs1574_cs1584_cs1581_cs1580_highlighting = hint
# temporary workaround for https://youtrack.jetbrains.com/issue/RIDER-130381/Rider-does-not-respect-propagated-NoWarn-CS1591?backToIssues=false
dotnet_diagnostic.CS1591.severity = none
#license header
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
steps:
- name: Checkout diffcalc-sheet-generator
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: ${{ inputs.id }}
repository: 'smoogipoo/diffcalc-sheet-generator'
+50 -17
View File
@@ -6,6 +6,7 @@ concurrency:
permissions:
contents: read # to fetch code (actions/checkout)
security-events: write # for reporting InspectCode issues
jobs:
inspect-code:
@@ -13,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
@@ -27,7 +28,7 @@ jobs:
run: dotnet restore osu.Desktop.slnf
- name: Restore inspectcode cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}
@@ -49,10 +50,14 @@ jobs:
exit $exit_code
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
uses: JetBrains/ReSharper-InspectCode@v0.12
with:
# this is WTF tier but if you don't specify *both* of these the defaults assume `build: true`
build: false
no-build: true
solution: ./osu.Desktop.slnf
caches-home: inspectcode
verbosity: WARN
test:
name: Test
@@ -71,10 +76,10 @@ jobs:
timeout-minutes: 120
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
@@ -96,30 +101,58 @@ jobs:
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
# https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#cancelled
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: ${{ always() }}
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
test-results:
name: Test results
runs-on: ubuntu-latest
# we want to wait for the `test` job to complete, but run regardless of whether it succeeds or fails
# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#example-not-requiring-successful-dependent-jobs
if: ${{ !cancelled() }}
needs: test
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download results
uses: actions/download-artifact@v8
with:
pattern: osu-test-results-*
merge-multiple: true
- name: Add test results summary to workflow run
uses: dorny/test-reporter@v3.0.0
with:
name: Results
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
list-tests: 'failed'
use-actions-summary: 'true'
build-only-android:
name: Build only (Android)
runs-on: windows-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup JDK 11
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: microsoft
java-version: 11
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
@@ -135,10 +168,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install .NET 8.0.x
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
+5 -4
View File
@@ -3,7 +3,8 @@ name: Pack and nuget
on:
push:
tags:
- '*'
- '*.*.*'
- '!*-*'
jobs:
notify_pending_production_deploy:
@@ -43,14 +44,14 @@ jobs:
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- 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
uses: actions/setup-dotnet@v5
with:
dotnet-version: "8.0.x"
@@ -76,7 +77,7 @@ jobs:
dotnet pack -c Release Templates /p:Version=${{ github.ref_name }} -o ${{steps.artifactsPath.outputs.nuget_artifacts}}
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: osu
path: |
-45
View File
@@ -1,45 +0,0 @@
# This is a workaround to allow PRs to report their coverage. This will run inside the base repository.
# See:
# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
name: Annotate CI run with test results
on:
workflow_run:
workflows: [ "Continuous Integration" ]
types:
- completed
permissions:
contents: read
actions: read
checks: write
jobs:
annotate:
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
with:
repository: ${{ github.event.workflow_run.repository.full_name }}
ref: ${{ github.event.workflow_run.head_sha }}
- name: Download results
uses: actions/download-artifact@v4
with:
pattern: osu-test-results-*
merge-multiple: true
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.8.0
with:
name: Results
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
list-tests: 'failed'
+2 -2
View File
@@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Create Sentry release
uses: getsentry/action-release@v1
uses: getsentry/action-release@v3
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ppy
@@ -38,8 +38,12 @@ jobs:
run: ./UseLocalOsu.sh
working-directory: ./osu-tools
- name: Build tools
run: dotnet build PerformanceCalculator --nologo --verbosity quiet
working-directory: ./osu-tools
- name: Regenerate mod definitions
run: dotnet run --project PerformanceCalculator -- mods > ../osu-web/database/mods.json
run: dotnet run --project PerformanceCalculator --no-build -- mods > ../osu-web/database/mods.json
working-directory: ./osu-tools
- name: Create pull request with changes
+13
View File
@@ -13,6 +13,19 @@
"preLaunchTask": "Build osu! (Debug)",
"console": "internalConsole"
},
{
"name": "osu! (Debug, Second Client)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Desktop/bin/Debug/net8.0/osu!.dll",
"--debug-client-id=1"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
"console": "internalConsole"
},
{
"name": "osu! (Release)",
"type": "coreclr",
+4 -3
View File
@@ -55,9 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library).
@@ -73,6 +71,9 @@ Aside from the above, below is a brief checklist of things to watch out when you
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
- Please pick the following target branch for your pull request:
- `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets,
- `master`, otherwise.
- Please avoid pushing untested or incomplete code.
- Please do not force-push or rebase unless we ask you to.
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
@@ -10,8 +10,8 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />
@@ -10,8 +10,8 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
@@ -10,8 +10,8 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />
@@ -10,8 +10,8 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.908.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.318.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+9 -12
View File
@@ -5,16 +5,13 @@
android:supportsRtl="true"
android:label="osu!"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher" />
<!-- for editor usage -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!--
READ_MEDIA_* permissions are available only on API 33 or greater. Devices with older android versions
don't understand the new permissions, so request the old READ_EXTERNAL_STORAGE permission to get storage access.
Since the old permission has no effect on >= API 33, don't request it.
Care needs to be taken to ensure runtime permission checks target the correct permission for the API level.
-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
android:roundIcon="@mipmap/ic_launcher">
<provider android:name="androidx.core.content.FileProvider"
android:authorities="sh.ppy.osulazer.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!-- https://developer.android.com/reference/androidx/core/content/FileProvider -->
<external-files-path path="logs" name="logs" />
<external-files-path path="exports" name="exports" />
</paths>
+2 -1
View File
@@ -58,6 +58,7 @@ namespace osu.Desktop
private readonly RichPresence presence = new RichPresence
{
Assets = new Assets { LargeImageKey = "osu_logo_lazer" },
Timestamps = Timestamps.Now,
Secrets = new Secrets
{
JoinSecret = null,
@@ -189,7 +190,7 @@ namespace osu.Desktop
}
// user party
if (!hideIdentifiableInformation && multiplayerClient.Room != null)
if (!hideIdentifiableInformation && multiplayerClient.Room != null && multiplayerClient.Room.Settings.MatchType != MatchType.Matchmaking)
{
MultiplayerRoom room = multiplayerClient.Room;
@@ -0,0 +1,55 @@
// 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.IO;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
namespace osu.Desktop.MacOS
{
/// <summary>
/// Checks if the game is located at `Applications` folder and displays a warning notification if not so.
/// </summary>
public partial class MacOSAppLocationChecker : Component
{
[Resolved]
private INotificationOverlay notification { get; set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
string assemblyPath = RuntimeInfo.EntryAssembly.Location;
bool inRootApp = assemblyPath.StartsWith("/Applications/", StringComparison.Ordinal);
bool inUserApp = assemblyPath.StartsWith(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications/"), StringComparison.Ordinal);
if (!inRootApp && !inUserApp)
notification.Post(new MacOSAppLocationNotification());
Expire();
}
private partial class MacOSAppLocationNotification : SimpleNotification
{
public MacOSAppLocationNotification()
{
Text = NotificationsStrings.MacOSAppLocation(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile));
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Icon = FontAwesome.Solid.ShieldAlt;
IconContent.Colour = colours.YellowDark;
}
}
}
}
+20 -8
View File
@@ -5,7 +5,6 @@ using System;
using System.IO;
using System.Reflection;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Microsoft.Win32;
using osu.Desktop.Performance;
using osu.Desktop.Security;
@@ -15,12 +14,12 @@ using osu.Desktop.Updater;
using osu.Framework;
using osu.Framework.Logging;
using osu.Game.Updater;
using osu.Desktop.MacOS;
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;
using osu.Game.Performance;
using osu.Game.Utils;
@@ -123,7 +122,7 @@ namespace osu.Desktop
public override bool RestartAppWhenExited()
{
Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
RestartOnExitAction = () => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId);
return true;
}
@@ -133,8 +132,17 @@ namespace osu.Desktop
LoadComponentAsync(new DiscordRichPresence(), Add);
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
break;
case RuntimeInfo.Platform.macOS when !IsPackageManaged && IsDeployedBuild:
if (!IsPackageManaged && IsDeployedBuild)
LoadComponentAsync(new MacOSAppLocationChecker(), Add);
break;
}
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
@@ -146,9 +154,13 @@ namespace osu.Desktop
{
base.SetHost(host);
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
if (iconStream != null)
host.Window.SetIconFromStream(iconStream);
// Apple operating systems use a better icon provided via external assets.
if (!RuntimeInfo.IsApple)
{
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
if (iconStream != null)
host.Window.SetIconFromStream(iconStream);
}
host.Window.Title = Name;
}
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
@@ -26,13 +27,15 @@ namespace osu.Desktop.Security
if (Environment.IsPrivilegedProcess)
notifications.Post(new ElevatedPrivilegesNotification());
Expire();
}
private partial class ElevatedPrivilegesNotification : SimpleNotification
{
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.";
Text = NotificationsStrings.ElevatedPrivileges(RuntimeInfo.IsUnix ? "root" : "Administrator");
}
[BackgroundDependencyLoader]
+4 -4
View File
@@ -146,11 +146,11 @@ namespace osu.Desktop.Updater
action();
}
private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update) => Task.Run(async () =>
private void restartToApplyUpdate(Velopack.UpdateManager updateManager, UpdateInfo update)
{
await updateManager.WaitExitThenApplyUpdatesAsync(update.TargetFullRelease).ConfigureAwait(false);
Schedule(() => game.AttemptExit());
});
game.RestartOnExitAction = () => updateManager.WaitExitThenApplyUpdates(update.TargetFullRelease);
game.AttemptExit();
}
private static void log(string text) => Logger.Log($"VelopackUpdateManager: {text}");
}
+2 -2
View File
@@ -24,8 +24,8 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="9.0.2" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="System.IO.Packaging" Version="10.0.5" />
<PackageReference Include="DiscordRichPresence" Version="1.6.1.70" />
<PackageReference Include="Velopack" Version="0.0.1298" />
</ItemGroup>
<ItemGroup Label="Resources">
@@ -5,7 +5,7 @@ using BenchmarkDotNet.Attributes;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Tests.NonVisual.Filtering;
namespace osu.Game.Benchmarks
{
@@ -42,7 +42,7 @@ namespace osu.Game.Benchmarks
Status = BeatmapOnlineStatus.Loved
};
private CarouselBeatmap carouselBeatmap = null!;
private FilterMatchingTest.CarouselBeatmap carouselBeatmap = null!;
private FilterCriteria criteria1 = null!;
private FilterCriteria criteria2 = null!;
private FilterCriteria criteria3 = null!;
@@ -55,7 +55,7 @@ namespace osu.Game.Benchmarks
var beatmap = getExampleBeatmap();
beatmap.OnlineID = 20201010;
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 };
carouselBeatmap = new CarouselBeatmap(beatmap);
carouselBeatmap = new FilterMatchingTest.CarouselBeatmap(beatmap);
criteria1 = new FilterCriteria();
criteria2 = new FilterCriteria
{
@@ -0,0 +1,77 @@
// 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.IO;
using BenchmarkDotNet.Attributes;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Tests.Resources;
namespace osu.Game.Benchmarks
{
public class BenchmarkDifficultyCalculation : BenchmarkTest
{
private DifficultyCalculator osuCalculator = null!;
private DifficultyCalculator taikoCalculator = null!;
private DifficultyCalculator catchCalculator = null!;
private DifficultyCalculator maniaCalculator = null!;
public override void SetUp()
{
using var resources = new DllResourceStore(typeof(TestResources).Assembly);
using var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz");
using var archiveReader = new ZipArchiveReader(archive);
var osuBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Gamu) [Insane].osu");
var taikoBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (MMzz) [Oni].osu");
var catchBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (Deif) [Salad].osu");
var maniaBeatmap = readBeatmap(archiveReader, "Soleily - Renatus (ExPew) [Another].osu");
osuCalculator = new OsuRuleset().CreateDifficultyCalculator(osuBeatmap);
taikoCalculator = new TaikoRuleset().CreateDifficultyCalculator(taikoBeatmap);
catchCalculator = new CatchRuleset().CreateDifficultyCalculator(catchBeatmap);
maniaCalculator = new ManiaRuleset().CreateDifficultyCalculator(maniaBeatmap);
}
private WorkingBeatmap readBeatmap(ZipArchiveReader archiveReader, string beatmapName)
{
using var beatmapStream = new MemoryStream();
archiveReader.GetStream(beatmapName).CopyTo(beatmapStream);
beatmapStream.Seek(0, SeekOrigin.Begin);
using var reader = new LineBufferedReader(beatmapStream);
var decoder = Beatmaps.Formats.Decoder.GetDecoder<Beatmap>(reader);
return new FlatWorkingBeatmap(decoder.Decode(reader));
}
[Benchmark]
public void CalculateDifficultyOsu() => osuCalculator.Calculate();
[Benchmark]
public void CalculateDifficultyTaiko() => taikoCalculator.Calculate();
[Benchmark]
public void CalculateDifficultyCatch() => catchCalculator.Calculate();
[Benchmark]
public void CalculateDifficultyMania() => maniaCalculator.Calculate();
[Benchmark]
public void CalculateDifficultyOsuHundredTimes()
{
for (int i = 0; i < 100; i++)
{
osuCalculator.Calculate();
}
}
}
}
@@ -7,9 +7,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="nunit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="nunit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
</ItemGroup>
<ItemGroup>
+1 -3
View File
@@ -35,11 +35,9 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using NUnit.Framework.Legacy;
using osu.Framework.IO.Stores;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
@@ -21,9 +22,9 @@ namespace osu.Game.Rulesets.Catch.Tests
var skinSource = new SkinProvidingContainer(rawSkin);
var skin = new CatchLegacySkinTransformer(skinSource);
Assert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value);
Assert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value);
Assert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value);
ClassicAssert.AreEqual(new Color4(232, 185, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDash)?.Value);
ClassicAssert.AreEqual(new Color4(232, 74, 35, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashAfterImage)?.Value);
ClassicAssert.AreEqual(new Color4(0, 255, 255, 255), skin.GetConfig<CatchSkinColour, Color4>(CatchSkinColour.HyperDashFruit)?.Value);
}
private class TestLegacySkin : LegacySkin
@@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Catch.Tests
}
[Test]
public void TestTinyDropletMissPreservesCatcherState()
public void TestTinyDropletMissChangesCatcherState()
{
AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit
{
@@ -165,8 +165,8 @@ namespace osu.Game.Rulesets.Catch.Tests
}));
AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet()));
AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 }));
// catcher state and hyper dash state is preserved
checkState(CatcherAnimationState.Kiai);
// catcher state is changed but hyper dash state is preserved
checkState(CatcherAnimationState.Fail);
checkHyperDash(true);
}
@@ -193,20 +193,20 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public Color4 HyperDashColour
{
get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDash)];
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDash)] = value;
}
public Color4 HyperDashAfterImageColour
{
get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashAfterImage)];
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashAfterImage)] = value;
}
public Color4 HyperDashFruitColour
{
get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
get => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)];
set => Configuration.CustomColours[nameof(CatchSkinColour.HyperDashFruit)] = value;
}
public TestSkin()
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
+8 -2
View File
@@ -155,6 +155,7 @@ namespace osu.Game.Rulesets.Catch
new CatchModMuted(),
new CatchModNoScope(),
new CatchModMovingFast(),
new CatchModSynesthesia(),
};
case ModType.System:
@@ -176,15 +177,20 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
protected override IEnumerable<HitResult> GetValidHitResults()
public override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Great,
HitResult.Miss,
HitResult.LargeTickHit,
HitResult.LargeTickMiss,
HitResult.SmallTickHit,
HitResult.SmallTickMiss,
HitResult.LargeBonus,
HitResult.IgnoreHit,
HitResult.IgnoreMiss,
};
}
@@ -300,7 +306,7 @@ namespace osu.Game.Rulesets.Catch
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"))
new RulesetBeatmapAttribute.AdditionalMetric("Fade-in time", LocalisableString.Interpolate($@"{IBeatmapDifficultyInfo.DifficultyRangeInt(effectiveDifficulty.ApproachRate, CatchHitObject.PREEMPT_RANGE):#,0.##} ms"))
]
};
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, effectiveDifficulty.DrainRate, 10)
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private float halfCatcherWidth;
public override int Version => 20250306;
public override int Version => 20251020;
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap)
@@ -3,13 +3,13 @@
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;
using osu.Game.Scoring.Legacy;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Catch.Difficulty
{
@@ -51,15 +51,13 @@ namespace osu.Game.Rulesets.Catch.Difficulty
// Combo scaling
if (catchAttributes.MaxCombo > 0)
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
value *= Math.Min(Math.Pow(score.MaxCombo, 0.35) / Math.Pow(catchAttributes.MaxCombo, 0.35), 1.0);
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;
double clockRate = ModUtils.CalculateRateWithMods(score.Mods);
// 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;
@@ -0,0 +1,65 @@
// 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.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Catch.Difficulty.Evaluators
{
public static class MovementEvaluator
{
private const double direction_change_bonus = 21.0;
public static double EvaluateDifficultyOf(DifficultyHitObject current, double catcherSpeedMultiplier)
{
var catchCurrent = (CatchDifficultyHitObject)current;
var catchLast = (CatchDifficultyHitObject)current.Previous(0);
var catchLastLast = (CatchDifficultyHitObject)current.Previous(1);
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
double distanceAddition = (Math.Pow(Math.Abs(catchCurrent.DistanceMoved), 1.3) / 510);
double sqrtStrain = Math.Sqrt(weightedStrainTime);
double edgeDashBonus = 0;
// Direction change bonus.
if (Math.Abs(catchCurrent.DistanceMoved) > 0.1)
{
if (current.Index >= 1 && Math.Abs(catchLast.DistanceMoved) > 0.1 && Math.Sign(catchCurrent.DistanceMoved) != Math.Sign(catchLast.DistanceMoved))
{
double bonusFactor = Math.Min(50, Math.Abs(catchCurrent.DistanceMoved)) / 50;
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(catchLast.DistanceMoved)) / 70, 0.38);
distanceAddition += direction_change_bonus / Math.Sqrt(catchLast.StrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
}
// Base bonus for every movement, giving some weight to streams.
distanceAddition += 12.5 * Math.Min(Math.Abs(catchCurrent.DistanceMoved), CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2)
/ (CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 6) / sqrtStrain;
}
// Bonus for edge dashes.
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
{
if (!catchCurrent.LastObject.HyperDash)
edgeDashBonus += 5.7;
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_HALF_CATCHER_WIDTH 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_HALF_CATCHER_WIDTH)
if (current.Index >= 2 && Math.Abs(catchCurrent.ExactDistanceMoved) <= CatchDifficultyHitObject.NORMALIZED_HALF_CATCHER_WIDTH * 2
&& catchCurrent.ExactDistanceMoved == -catchLast.ExactDistanceMoved && catchLast.ExactDistanceMoved == -catchLastLast.ExactDistanceMoved
&& catchCurrent.StrainTime == catchLast.StrainTime && catchLast.StrainTime == catchLastLast.StrainTime)
distanceAddition = 0;
return distanceAddition / weightedStrainTime;
}
}
}
@@ -11,15 +11,49 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
{
public class CatchDifficultyHitObject : DifficultyHitObject
{
private const float normalized_hitobject_radius = 41.0f;
public const float NORMALIZED_HALF_CATCHER_WIDTH = 41.0f;
private const float absolute_player_positioning_error = 16.0f;
public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject;
public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject;
/// <summary>
/// Normalized position of <see cref="BaseObject"/>.
/// </summary>
public readonly float NormalizedPosition;
/// <summary>
/// Normalized position of <see cref="LastObject"/>.
/// </summary>
public readonly float LastNormalizedPosition;
/// <summary>
/// Normalized position of the player required to catch <see cref="BaseObject"/>, assuming the player moves as little as possible.
/// </summary>
public float PlayerPosition { get; private set; }
/// <summary>
/// Normalized position of the player after catching <see cref="LastObject"/>.
/// </summary>
public float LastPlayerPosition { get; private set; }
/// <summary>
/// Normalized distance between <see cref="LastPlayerPosition"/> and <see cref="PlayerPosition"/>.
/// </summary>
/// <remarks>
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
/// </remarks>
public float DistanceMoved { get; private set; }
/// <summary>
/// Normalized distance the player has to move from <see cref="LastPlayerPosition"/> in order to catch <see cref="BaseObject"/> at its <see cref="NormalizedPosition"/>.
/// </summary>
/// <remarks>
/// The sign of the value indicates the direction of the movement: negative is left and positive is right.
/// </remarks>
public float ExactDistanceMoved { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="CatchDifficultyHitObject"/>, with a minimum of 40ms.
/// </summary>
@@ -29,13 +63,35 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
: base(hitObject, lastObject, clockRate, objects, index)
{
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
float scalingFactor = NORMALIZED_HALF_CATCHER_WIDTH / halfCatcherWidth;
NormalizedPosition = BaseObject.EffectiveX * scalingFactor;
LastNormalizedPosition = LastObject.EffectiveX * scalingFactor;
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(40, DeltaTime);
setMovementState();
}
private void setMovementState()
{
LastPlayerPosition = Index == 0 ? LastNormalizedPosition : ((CatchDifficultyHitObject)Previous(0)).PlayerPosition;
PlayerPosition = Math.Clamp(
LastPlayerPosition,
NormalizedPosition - (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error),
NormalizedPosition + (NORMALIZED_HALF_CATCHER_WIDTH - absolute_player_positioning_error)
);
DistanceMoved = PlayerPosition - LastPlayerPosition;
// For the exact position we consider that the catcher is in the correct position for both objects
ExactDistanceMoved = NormalizedPosition - LastPlayerPosition;
// After a hyperdash we ARE in the correct position. Always!
if (LastObject.HyperDash)
PlayerPosition = NormalizedPosition;
}
}
}
@@ -1,8 +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.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Catch.Difficulty.Evaluators;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
@@ -11,10 +10,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{
public class Movement : StrainDecaySkill
{
private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f;
private const double direction_change_bonus = 21.0;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.2;
@@ -24,12 +19,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
protected readonly float HalfCatcherWidth;
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.
/// </summary>
@@ -49,80 +38,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
var catchCurrent = (CatchDifficultyHitObject)current;
lastPlayerPosition ??= catchCurrent.LastNormalizedPosition;
float playerPosition = Math.Clamp(
lastPlayerPosition.Value,
catchCurrent.NormalizedPosition - (normalized_hitobject_radius - absolute_player_positioning_error),
catchCurrent.NormalizedPosition + (normalized_hitobject_radius - absolute_player_positioning_error)
);
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);
double sqrtStrain = Math.Sqrt(weightedStrainTime);
double edgeDashBonus = 0;
// Direction change bonus.
if (Math.Abs(distanceMoved) > 0.1)
{
if (Math.Abs(lastDistanceMoved) > 0.1 && Math.Sign(distanceMoved) != Math.Sign(lastDistanceMoved))
{
double bonusFactor = Math.Min(50, Math.Abs(distanceMoved)) / 50;
double antiflowFactor = Math.Max(Math.Min(70, Math.Abs(lastDistanceMoved)) / 70, 0.38);
distanceAddition += direction_change_bonus / Math.Sqrt(lastStrainTime + 16) * bonusFactor * antiflowFactor * Math.Max(1 - Math.Pow(weightedStrainTime / 1000, 3), 0);
}
// Base bonus for every movement, giving some weight to streams.
distanceAddition += 12.5 * Math.Min(Math.Abs(distanceMoved), normalized_hitobject_radius * 2) / (normalized_hitobject_radius * 6) / sqrtStrain;
}
// Bonus for edge dashes.
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
{
if (!catchCurrent.LastObject.HyperDash)
edgeDashBonus += 5.7;
else
{
// After a hyperdash we ARE in the correct position. Always!
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
}
// 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;
return MovementEvaluator.EvaluateDifficultyOf(current, catcherSpeedMultiplier);
}
}
}
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private double placementStartTime;
private double placementEndTime;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
public BananaShowerPlacementBlueprint()
{
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private InputManager inputManager = null!;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
public JuiceStreamPlacementBlueprint()
{
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
@@ -224,7 +225,8 @@ namespace osu.Game.Rulesets.Catch.Edit
#region Clipboard handling
public override string ConvertSelectionToString()
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime)
.Select(h => (h.IndexInCurrentCombo + 1).ToString(CultureInfo.InvariantCulture)));
// 1,2,3,4 ...
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
@@ -51,7 +51,8 @@ namespace osu.Game.Rulesets.Catch.Mods
return string.Empty;
string format(string acronym, DifficultyBindable bindable) => $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
string format(string acronym, DifficultyBindable bindable)
=> $"{acronym}{bindable.Value!.Value.ToStandardFormattedString(1)}";
}
}
@@ -0,0 +1,60 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Edit;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Mods
{
/// <summary>
/// Mod that colours <see cref="HitObject"/>s based on the musical division they are on
/// </summary>
public class CatchModSynesthesia : ModSynesthesia, IApplicableToBeatmap, IApplicableToDrawableHitObject
{
private readonly OsuColour colours = new OsuColour();
private IBeatmap? currentBeatmap { get; set; }
public void ApplyToBeatmap(IBeatmap beatmap)
{
//Store a reference to the current beatmap to look up the beat divisor when notes are drawn
if (currentBeatmap != beatmap)
currentBeatmap = beatmap;
}
public void ApplyToDrawableHitObject(DrawableHitObject d)
{
if (currentBeatmap == null) return;
Color4? timingBasedColour = null;
d.HitObjectApplied += _ =>
{
// Block bananas from getting coloured.
if (d.HitObject is not Banana)
{
timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(d.HitObject.StartTime), colours);
}
// Colour droplets into a solid colour, as droplets aren't generated snapped to timeline ticks.
if (d.HitObject is Droplet)
{
timingBasedColour = Color4.LightGreen;
}
};
// Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour().
d.OnUpdate += _ =>
{
if (timingBasedColour != null)
d.AccentColour.Value = timingBasedColour.Value;
};
}
}
}
+17 -4
View File
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
@@ -53,13 +54,25 @@ namespace osu.Game.Rulesets.Catch.Objects
public override IEnumerable<string> LookupNames => lookup_names;
public BananaHitSampleInfo(int volume = 100)
: base(string.Empty, volume: volume)
public BananaHitSampleInfo()
: this(string.Empty)
{
}
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
=> new BananaHitSampleInfo(newVolume.GetOr(Volume));
public BananaHitSampleInfo(HitSampleInfo info)
: this(info.Name, info.Bank, info.Suffix, info.Volume, info.EditorAutoBank, info.UseBeatmapSamples)
{
}
private BananaHitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true, bool useBeatmapSamples = false)
: base(name, bank, suffix, volume, editorAutoBank, useBeatmapSamples)
{
}
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default, Optional<bool> newUseBeatmapSamples = default)
=> new BananaHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume),
newEditorAutoBank.GetOr(EditorAutoBank), newUseBeatmapSamples.GetOr(UseBeatmapSamples));
public bool Equals(BananaHitSampleInfo? other)
=> other != null;
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
StartTime = time,
BananaIndex = count,
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo().Volume) }
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(CreateHitSampleInfo()) }
});
count++;
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, PREEMPT_RANGE);
TimePreempt = IBeatmapDifficultyInfo.DifficultyRangeInt(difficulty.ApproachRate, PREEMPT_RANGE);
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
}
@@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
leaderboard.Origin = Anchor.CentreLeft;
leaderboard.X = 10;
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
Children = new Drawable[]
+14 -14
View File
@@ -224,7 +224,20 @@ namespace osu.Game.Rulesets.Catch.UI
addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
}
// droplet doesn't affect the catcher state
if (result.IsHit)
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
else if (hitObject is not Banana)
CurrentState = CatcherAnimationState.Fail;
if (palpableObject.HitObject.LastInCombo)
{
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
Explode();
else
Drop();
}
// droplet doesn't affect hyperdash state
if (hitObject is TinyDroplet) return;
// if a hyper fruit was already handled this frame, just go where it says to go.
@@ -244,19 +257,6 @@ namespace osu.Game.Rulesets.Catch.UI
else
SetHyperDashState();
}
if (result.IsHit)
CurrentState = hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle;
else if (!(hitObject is Banana))
CurrentState = CatcherAnimationState.Fail;
if (palpableObject.HitObject.LastInCombo)
{
if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result))
Explode();
else
Drop();
}
}
public void OnRevertResult(JudgementResult result)
+1 -3
View File
@@ -35,11 +35,9 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
@@ -0,0 +1,351 @@
// 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.Testing;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
using DragArea = osu.Game.Screens.Edit.Compose.Components.Timeline.TimelineHitObjectBlueprint.DragArea;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestSceneHoldNoteTailDrag : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("Clear objects", () => EditorBeatmap.Clear());
}
[Test]
public void TestSimpleTailDragForward()
{
AddStep("Add hold note", () =>
{
EditorBeatmap.Add(new HoldNote { StartTime = 2170, Duration = 937.5 });
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().Single();
dragForward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Duration is higher", () => ((HoldNote)EditorBeatmap.HitObjects.First())!.Duration > 937.5f);
}
[Test]
public void TestSimpleTailDragBackwards()
{
AddStep("Add hold note", () =>
{
EditorBeatmap.Add(new HoldNote { StartTime = 2170, Duration = 937.5 });
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().Single();
dragBackward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Duration is lower", () => ((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f);
}
[Test]
public void TestSamePositionButNotSelectedDragForward()
{
AddStep("Add hold notes", () =>
{
EditorBeatmap.AddRange([
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
]);
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
dragForward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Duration is higher, other is unchanged", () =>
((HoldNote)EditorBeatmap.HitObjects[0]).Duration > 937.5f &&
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration == 937.5f
);
}
[Test]
public void TestSamePositionButNotSelectedDragBackward()
{
AddStep("Add hold notes", () =>
{
EditorBeatmap.AddRange([
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
]);
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
dragBackward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Duration is lower, other is unchanged", () =>
((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f &&
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration == 937.5f
);
}
[Test]
public void TestSamePositionSelectedDragForward()
{
AddStep("Add hold notes", () =>
{
EditorBeatmap.AddRange([
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
]);
});
AddStep("Select all", () =>
{
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
dragForward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Both durations are higher", () =>
((HoldNote)EditorBeatmap.HitObjects[0]).Duration > 937.5f &&
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration > 937.5f
);
}
[Test]
public void TestSamePositionSelectedDragBackward()
{
AddStep("Add hold notes", () =>
{
EditorBeatmap.AddRange([
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
]);
});
AddStep("Select all", () =>
{
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
dragBackward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Both durations are lower", () =>
((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f &&
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f
);
}
[Test]
public void TestSelectedButDifferentPositions()
{
AddStep("Add hold notes", () =>
{
EditorBeatmap.AddRange([
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
new HoldNote { StartTime = 2404, Duration = 937.5, Column = 1 }
]);
});
AddStep("Select all", () =>
{
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
dragBackward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Duration is unchanged, other is lower", () =>
((HoldNote)EditorBeatmap.HitObjects[0]).Duration == 937.5f &&
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f
);
}
[Test]
public void TestSelectedSameStartTimeDifferentDurations()
{
AddStep("Add hold notes", () =>
{
EditorBeatmap.AddRange([
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
new HoldNote { StartTime = 2170, Duration = 1171.8, Column = 1 }
]);
});
AddStep("Select all", () =>
{
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
});
AddStep("Drag until both match", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
InputManager.MoveMouseTo(blueprintDragArea);
InputManager.PressKey(Key.LShift);
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(new Vector2(1000, 110));
});
AddStep("Continue the drag", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
dragBackward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Duration is unchanged, other is lower", () =>
((HoldNote)EditorBeatmap.HitObjects[0]).Duration == 937.5f &&
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f
);
}
[Test]
public void TestSelectedSameDurationDifferentStartTimes()
{
AddStep("Add hold notes", () =>
{
EditorBeatmap.AddRange([
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
new HoldNote { StartTime = 2638.7, Duration = 937.5, Column = 1 }
]);
});
AddStep("Select all", () =>
{
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
dragBackward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Duration is unchanged, other is lower", () =>
((HoldNote)EditorBeatmap.HitObjects[0]).Duration == 937.5f &&
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration < 937.5f
);
}
[Test]
public void TestDragNoteOutsideOfSelection()
{
AddStep("Add hold notes", () =>
{
EditorBeatmap.AddRange([
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 1 }
]);
});
AddStep("Select the back stack slider", () =>
{
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.Last());
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
dragBackward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Duration is lower, other is unchanged", () =>
((HoldNote)EditorBeatmap.HitObjects[0]).Duration < 937.5f &&
((HoldNote)EditorBeatmap.HitObjects[^1]).Duration == 937.5f
);
}
[Test]
public void TestDragHoldNoteWithNotes()
{
AddStep("Add notes", () =>
{
EditorBeatmap.AddRange([
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 0 },
new Note { StartTime = 2170, Column = 1 },
new Note { StartTime = 3107.5, Column = 2 },
new HoldNote { StartTime = 2170, Duration = 937.5, Column = 3 }
]);
});
AddStep("Select all", () =>
{
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects);
});
AddStep("Drag tail", () =>
{
var blueprintDragArea = this.ChildrenOfType<DragArea>().First();
dragBackward(blueprintDragArea);
});
AddStep("Release tail", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("Both durations are lower", () =>
{
var holdNotes = EditorBeatmap.HitObjects.OfType<HoldNote>();
return holdNotes.First().Duration < 937.5f && holdNotes.Last().Duration < 937.5f;
}
);
}
private void dragForward(DragArea dragArea)
{
InputManager.MoveMouseTo(dragArea);
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(new Vector2(1100, 110));
}
private void dragBackward(DragArea dragArea)
{
InputManager.MoveMouseTo(dragArea);
InputManager.PressButton(MouseButton.Left);
InputManager.MoveMouseTo(new Vector2(700, 110));
}
}
}
@@ -5,6 +5,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
@@ -16,6 +17,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK.Input;
@@ -36,21 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestPlaceBeforeCurrentTimeDownwards()
{
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
AddStep("move mouse before current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("note start time < 0", () => getNote().StartTime < 0);
}
[Test]
public void TestPlaceAfterCurrentTimeDownwards()
{
AddStep("move mouse after current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
@@ -58,7 +47,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("note start time > 0", () => getNote().StartTime > 0);
AddAssert("note start time < 200", () => getNote().StartTime < 200);
}
[Test]
public void TestPlaceAfterCurrentTimeDownwards()
{
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
AddStep("move mouse after current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(300));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("note start time > 200", () => getNote().StartTime > 200);
}
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddStep("change seek setting to true", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, true));
placeObject();
AddUntilStep("wait for seek to complete", () => !EditorClock.IsSeeking);
AddAssert("seeked forward to object", () => EditorClock.CurrentTime, () => Is.GreaterThan(initialTime));
AddAssert("seeked forward to object", () => EditorClock.CurrentTime, () => Is.GreaterThan(initialTime!));
}
[Test]
@@ -18,15 +18,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public void TestNormalSelection()
{
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)>
{ (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) }
));
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, [(5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1)]));
addReset();
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)>
{ (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) }
));
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, [(42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1)]));
addReset();
AddStep("add notes to row", () =>
@@ -41,15 +37,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
EditorBeatmap.AddRange(new[] { second, third, forth });
});
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)>
{ (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) }
));
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, [(11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3)]));
addReset();
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)>
{ (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) }
));
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, [(96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1)]));
}
[Test]
public void TestRoundingToNearestMillisecondApplied()
{
AddStep("resnap note to have fractional coordinates",
() => EditorBeatmap.HitObjects.OfType<ManiaHitObject>().Single(ho => ho.StartTime == 85_373 && ho.Column == 1).StartTime = 85_373.125);
addStepClickLink("01:25:373 (85373|1)");
AddAssert("selected note", () => checkSnapAndSelectColumn(85_373.125, [(85_373.125, 1)]));
}
[Test]
@@ -75,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
private void addReset() => addStepClickLink("00:00:000", "reset", false);
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null)
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(double, int)>? columnPairs = null)
{
bool checkColumns = columnPairs != null
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[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")]
[TestCase("spinner-convert-samples")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using NUnit.Framework.Legacy;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Screens.Select;
@@ -18,19 +19,19 @@ namespace osu.Game.Rulesets.Mania.Tests
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1");
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria
{
@@ -44,19 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "1,3,5,7");
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria
{
@@ -70,19 +71,19 @@ namespace osu.Game.Rulesets.Mania.Tests
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1");
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria
{
@@ -96,19 +97,19 @@ namespace osu.Game.Rulesets.Mania.Tests
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "1,3,5,7");
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria
{
@@ -122,23 +123,23 @@ namespace osu.Game.Rulesets.Mania.Tests
var criteria = new ManiaFilterCriteria();
criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4");
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 1 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 2 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new RulesetInfo { OnlineID = 0 }, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria
{
@@ -153,23 +154,23 @@ namespace osu.Game.Rulesets.Mania.Tests
criteria.TryParseCustomKeywordCriteria("keys", Operator.Greater, "4");
criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "7");
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 3 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 4 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 5 }),
new FilterCriteria()));
Assert.False(criteria.Matches(
ClassicAssert.False(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 7 }),
new FilterCriteria()));
Assert.True(criteria.Matches(
ClassicAssert.True(criteria.Matches(
new BeatmapInfo(new ManiaRuleset().RulesetInfo, new BeatmapDifficulty { CircleSize = 9 }),
new FilterCriteria()));
}
@@ -179,9 +180,9 @@ namespace osu.Game.Rulesets.Mania.Tests
{
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"));
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.Equal, "some text"));
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.NotEqual, "4,some text"));
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("keys", Operator.GreaterOrEqual, "4,5,6"));
}
[TestCase]
@@ -199,7 +200,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 0,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo1, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0");
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
@@ -207,7 +208,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 100,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo2, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "100");
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
@@ -215,7 +216,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 100,
EndTimeObjectCount = 100
};
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo3, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "1");
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
@@ -223,7 +224,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 100,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo4, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "0.1");
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
@@ -231,7 +232,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 1000,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo5, filterCriteria));
}
[TestCase]
@@ -249,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 0,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo1, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo1, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0");
BeatmapInfo beatmapInfo2 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
@@ -257,7 +258,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 100,
EndTimeObjectCount = 0
};
Assert.True(criteria.Matches(beatmapInfo2, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo2, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "100");
BeatmapInfo beatmapInfo3 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
@@ -265,7 +266,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 100,
EndTimeObjectCount = 100
};
Assert.True(criteria.Matches(beatmapInfo3, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo3, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1");
BeatmapInfo beatmapInfo4 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
@@ -273,7 +274,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 100,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo4, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo4, filterCriteria));
criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "0.1");
BeatmapInfo beatmapInfo5 = new BeatmapInfo(new ManiaRuleset().RulesetInfo)
@@ -281,7 +282,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 1000,
EndTimeObjectCount = 1
};
Assert.True(criteria.Matches(beatmapInfo5, filterCriteria));
ClassicAssert.True(criteria.Matches(beatmapInfo5, filterCriteria));
}
[TestCase]
@@ -299,7 +300,7 @@ namespace osu.Game.Rulesets.Mania.Tests
TotalObjectCount = 100,
EndTimeObjectCount = 50
};
Assert.False(criteria.Matches(beatmapInfo, filterCriteria));
ClassicAssert.False(criteria.Matches(beatmapInfo, filterCriteria));
}
[TestCase]
@@ -307,8 +308,8 @@ namespace osu.Game.Rulesets.Mania.Tests
{
var criteria = new ManiaFilterCriteria();
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text"));
Assert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text"));
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.Equal, "some text"));
ClassicAssert.False(criteria.TryParseCustomKeywordCriteria("lns", Operator.GreaterOrEqual, "1some text"));
}
}
}
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.Beatmaps;
using NUnit.Framework;
using NUnit.Framework.Legacy;
namespace osu.Game.Rulesets.Mania.Tests
{
@@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
var definition = new StageDefinition(columns);
var results = getResults(definition);
Assert.AreEqual(special, results);
ClassicAssert.AreEqual(special, results);
}
private IEnumerable<bool> getResults(StageDefinition definition)
@@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
@@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
public void TestMapHasNoHoldNotes()
{
var testBeatmap = createModdedBeatmap();
Assert.False(testBeatmap.HitObjects.OfType<HoldNote>().Any());
ClassicAssert.False(testBeatmap.HitObjects.OfType<HoldNote>().Any());
}
[Test]
@@ -0,0 +1,10 @@
osu file format v14
[General]
Mode: 0
[TimingPoints]
0,300,4,0,2,100,1,0
[HitObjects]
444,320,1000,5,2,0:0:0:0:
@@ -0,0 +1,16 @@
{
"Mappings": [{
"StartTime": 1000.0,
"Objects": [{
"StartTime": 1000.0,
"EndTime": 8000.0,
"Column": 0,
"PlaySlidingSamples": false,
"NodeSamples": [
["Gameplay/soft-hitnormal"],
["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"]
],
"Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"],
}]
}]
}
@@ -0,0 +1,18 @@
osu file format v14
[General]
Mode: 0
[Difficulty]
HPDrainRate:5
CircleSize:5
OverallDifficulty:5
ApproachRate:5
SliderMultiplier:1.4
SliderTickRate:1
[TimingPoints]
0,500,4,2,0,100,1,0
[HitObjects]
256,192,1000,8,4,8000,0:2:0:0:
@@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@@ -33,11 +34,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
ClassicAssert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
}
[Test]
@@ -54,11 +55,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
ClassicAssert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
}
[Test]
@@ -74,11 +75,11 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
ClassicAssert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
}
[Test]
@@ -96,13 +97,13 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 2, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
ClassicAssert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
}
[Test]
@@ -119,15 +120,15 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time");
Assert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time");
Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
ClassicAssert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect first note release time");
ClassicAssert.AreEqual(2000, generated.Frames[frame_offset + 2].Time, "Incorrect second note hit time");
ClassicAssert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
ClassicAssert.True(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been pressed");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
}
[Test]
@@ -146,16 +147,16 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
Assert.AreEqual(4000, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has been released");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 4, "Incorrect number of frames");
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
ClassicAssert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
ClassicAssert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
ClassicAssert.AreEqual(4000, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
ClassicAssert.True(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released");
ClassicAssert.True(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has been released");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 3], ManiaAction.Key2), "Key2 has not been released");
}
[Test]
@@ -173,14 +174,14 @@ namespace osu.Game.Rulesets.Mania.Tests
var generated = new ManiaAutoGenerator(beatmap).Generate();
Assert.AreEqual(generated.Frames.Count, frame_offset + 3, "Incorrect number of frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time");
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key2), "Key2 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been released");
ClassicAssert.AreEqual(generated.Frames.Count, frame_offset + 3, "Incorrect number of frames");
ClassicAssert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
ClassicAssert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect second note press time + first note release time");
ClassicAssert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect second note release time");
ClassicAssert.True(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1), "Key1 has not been released");
ClassicAssert.True(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key2), "Key2 has not been pressed");
ClassicAssert.False(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key2), "Key2 has not been released");
}
private bool checkContains(ReplayFrame frame, params ManiaAction[] actions) => actions.All(action => ((ManiaReplayFrame)frame).Actions.Contains(action));
@@ -10,6 +10,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
@@ -505,6 +506,29 @@ namespace osu.Game.Rulesets.Mania.Tests
.All(j => j.Type.IsHit()));
}
/// <summary>
/// This ensures that the value of <see cref="DrawableHoldNote.MissingStartTime"/>
/// will be set correctly when the body receives a judgment during the hold.
///
/// -----[ ]-----
/// x o
/// </summary>
[Test]
public void TestReleaseDuringHoldMissingStartTime()
{
performTest([
new ManiaReplayFrame(time_head, ManiaAction.Key1),
new ManiaReplayFrame(time_during_hold_1)
]);
assertHeadJudgement(HitResult.Perfect);
assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.IgnoreMiss);
AddAssert("body judgement is miss", () => !judgementResults.Single(j => j.HitObject is HoldNoteBody).IsHit);
AddAssert("body judgement time indicates during hold", () => judgementResults.Single(j => j.HitObject is HoldNoteBody).TimeAbsolute, () => Is.EqualTo(time_during_hold_1).Within(100));
}
private void assertHitObjectJudgement(HitObject hitObject, HitResult result)
=> AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result));
@@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
AssertBeatmapLookup(expected_sample);
AssertNoLookup(unwanted_sample);
}
[Test]
public void TestConvertHitObjectCustomSampleBank()
{
const string beatmap_sample = "normal-hitwhistle2";
const string user_skin_sample = "normal-hitnormal";
SetupSkins(beatmap_sample, user_skin_sample);
CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(beatmap_sample);
AssertUserLookup(user_skin_sample);
}
}
}
@@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="NUnit" Version="4.5.1" />
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
@@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Duration = endTime - HitObject.StartTime,
Column = column,
Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
NodeSamples =
[
HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(),
HitObject.Samples
]
};
}
else
@@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
{
public class IndividualStrainEvaluator
{
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
double startTime = maniaCurrent.StartTime;
double endTime = maniaCurrent.EndTime;
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
// We award a bonus if this note starts and ends before the end of another hold note.
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
{
if (maniaPrevious is null)
continue;
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
{
holdFactor = 1.25;
break;
}
}
return 2.0 * holdFactor;
}
}
}
@@ -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;
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Mania.Difficulty.Evaluators
{
public class OverallStrainEvaluator
{
private const double release_threshold = 30;
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
double startTime = maniaCurrent.StartTime;
double endTime = maniaCurrent.EndTime;
bool isOverlapping = false;
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
foreach (var maniaPrevious in maniaCurrent.PreviousHitObjects)
{
if (maniaPrevious is null)
continue;
// The current note is overlapped if a previous note or end is overlapping the current note body
isOverlapping |= Precision.DefinitelyBigger(maniaPrevious.EndTime, startTime, 1) &&
Precision.DefinitelyBigger(endTime, maniaPrevious.EndTime, 1) &&
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1);
// We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(maniaPrevious.EndTime, endTime, 1) &&
Precision.DefinitelyBigger(startTime, maniaPrevious.StartTime, 1))
holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - maniaPrevious.EndTime));
}
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// holdAddition
// ^
// 1.0 + - - - - - -+-----------
// | /
// 0.5 + - - - - -/ Sigmoid Curve
// | /|
// 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold
if (isOverlapping)
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
return (1 + holdAddition) * holdFactor;
}
}
}
@@ -65,13 +65,22 @@ namespace osu.Game.Rulesets.Mania.Difficulty
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
var sortedObjects = beatmap.HitObjects.ToArray();
int totalColumns = ((ManiaBeatmap)beatmap).TotalColumns;
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
List<DifficultyHitObject>[] perColumnObjects = new List<DifficultyHitObject>[totalColumns];
for (int column = 0; column < totalColumns; column++)
perColumnObjects[column] = new List<DifficultyHitObject>();
for (int i = 1; i < sortedObjects.Length; i++)
objects.Add(new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count));
{
var currentObject = new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, perColumnObjects, objects.Count);
objects.Add(currentObject);
perColumnObjects[currentObject.Column].Add(currentObject);
}
return objects;
}
@@ -12,9 +12,59 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing
{
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, int index)
private readonly List<DifficultyHitObject>[] perColumnObjects;
private readonly int columnIndex;
public readonly int Column;
// The hit object earlier in time than this note in each column
public readonly ManiaDifficultyHitObject?[] PreviousHitObjects;
public readonly double ColumnStrainTime;
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, List<DifficultyHitObject>[] perColumnObjects, int index)
: base(hitObject, lastObject, clockRate, objects, index)
{
int totalColumns = perColumnObjects.Length;
this.perColumnObjects = perColumnObjects;
Column = BaseObject.Column;
columnIndex = perColumnObjects[Column].Count;
PreviousHitObjects = new ManiaDifficultyHitObject[totalColumns];
ColumnStrainTime = StartTime - PrevInColumn(0)?.StartTime ?? StartTime;
if (index > 0)
{
ManiaDifficultyHitObject prevNote = (ManiaDifficultyHitObject)objects[index - 1];
for (int i = 0; i < prevNote.PreviousHitObjects.Length; i++)
PreviousHitObjects[i] = prevNote.PreviousHitObjects[i];
// intentionally depends on processing order to match live.
PreviousHitObjects[prevNote.Column] = prevNote;
}
}
/// <summary>
/// The previous object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
/// </summary>
/// <param name="backwardsIndex">The number of notes to go back.</param>
/// <returns>The object in this column <paramref name="backwardsIndex"/> notes back, or null if this is the first note in the column.</returns>
public ManiaDifficultyHitObject? PrevInColumn(int backwardsIndex)
{
int index = columnIndex - (backwardsIndex + 1);
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
}
/// <summary>
/// The next object in the same column as this <see cref="ManiaDifficultyHitObject"/>, exclusive of Long Note tails.
/// </summary>
/// <param name="forwardsIndex">The number of notes to go forward.</param>
/// <returns>The object in this column <paramref name="forwardsIndex"/> notes forward, or null if this is the last note in the column.</returns>
public ManiaDifficultyHitObject? NextInColumn(int forwardsIndex)
{
int index = columnIndex + (forwardsIndex + 1);
return index >= 0 && index < perColumnObjects[Column].Count ? (ManiaDifficultyHitObject)perColumnObjects[Column][index] : null;
}
}
}
@@ -2,10 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mania.Difficulty.Evaluators;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
@@ -15,23 +14,17 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
private const double release_threshold = 30;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
private readonly double[] startTimes;
private readonly double[] endTimes;
private readonly double[] individualStrains;
private double individualStrain;
private double highestIndividualStrain;
private double overallStrain;
public Strain(Mod[] mods, int totalColumns)
: base(mods)
{
startTimes = new double[totalColumns];
endTimes = new double[totalColumns];
individualStrains = new double[totalColumns];
overallStrain = 1;
}
@@ -39,65 +32,24 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
double startTime = maniaCurrent.StartTime;
double endTime = maniaCurrent.EndTime;
int column = maniaCurrent.BaseObject.Column;
bool isOverlapping = false;
double closestEndTime = Math.Abs(endTime - startTime); // Lowest value we can assume with the current information
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
individualStrains[maniaCurrent.Column] = applyDecay(individualStrains[maniaCurrent.Column], maniaCurrent.ColumnStrainTime, individual_decay_base);
individualStrains[maniaCurrent.Column] += IndividualStrainEvaluator.EvaluateDifficultyOf(current);
for (int i = 0; i < endTimes.Length; ++i)
{
// The current note is overlapped if a previous note or end is overlapping the current note body
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) &&
Precision.DefinitelyBigger(endTime, endTimes[i], 1) &&
Precision.DefinitelyBigger(startTime, startTimes[i], 1);
// Take the hardest individualStrain for notes that happen at the same time (in a chord).
// This is to ensure the order in which the notes are processed does not affect the resultant total strain.
highestIndividualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(highestIndividualStrain, individualStrains[maniaCurrent.Column]) : individualStrains[maniaCurrent.Column];
// We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) &&
Precision.DefinitelyBigger(startTime, startTimes[i], 1))
holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
}
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// holdAddition
// ^
// 1.0 + - - - - - -+-----------
// | /
// 0.5 + - - - - -/ Sigmoid Curve
// | /|
// 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold
if (isOverlapping)
holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
// Decay and increase individualStrains in own column
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);
individualStrains[column] += 2.0 * holdFactor;
// For notes at the same time (in a chord), the individualStrain should be the hardest individualStrain out of those columns
individualStrain = maniaCurrent.DeltaTime <= 1 ? Math.Max(individualStrain, individualStrains[column]) : individualStrains[column];
// Decay and increase overallStrain
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base);
overallStrain += (1 + holdAddition) * holdFactor;
// Update startTimes and endTimes arrays
startTimes[column] = startTime;
endTimes[column] = endTime;
overallStrain = applyDecay(overallStrain, maniaCurrent.DeltaTime, overall_decay_base);
overallStrain += OverallStrainEvaluator.EvaluateDifficultyOf(current);
// By subtracting CurrentStrain, this skill effectively only considers the maximum strain of any one hitobject within each strain section.
return individualStrain + overallStrain - CurrentStrain;
return highestIndividualStrain + overallStrain - CurrentStrain;
}
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current)
=> applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current) =>
applyDecay(highestIndividualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
private double applyDecay(double value, double deltaTime, double decayBase)
=> value * Math.Pow(decayBase, deltaTime / 1000);
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
public HoldNotePlacementBlueprint()
: base(new HoldNote())
@@ -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;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@@ -54,7 +56,8 @@ namespace osu.Game.Rulesets.Mania.Edit
};
public override string ConvertSelectionToString()
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime)
.Select(h => FormattableString.Invariant($"{Math.Round(h.StartTime)}|{h.Column}")));
// 123|0,456|1,789|2 ...
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
@@ -73,10 +76,10 @@ namespace osu.Game.Rulesets.Mania.Edit
if (split.Length != 2)
continue;
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
if (!int.TryParse(split[0], out int time) || !int.TryParse(split[1], out int column))
continue;
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => Precision.AlmostEquals(h.StartTime, time, 0.5) && h.Column == column);
if (current == null)
continue;
+5 -3
View File
@@ -383,7 +383,7 @@ namespace osu.Game.Rulesets.Mania
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderDescending().First(v => variant >= v);
}
protected override IEnumerable<HitResult> GetValidHitResults()
public override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
@@ -392,9 +392,11 @@ namespace osu.Game.Rulesets.Mania
HitResult.Good,
HitResult.Ok,
HitResult.Meh,
HitResult.Miss,
// HitResult.SmallBonus is used for awarding perfect bonus score but is not included here as
// it would be a bit redundant to show this to the user.
HitResult.IgnoreHit,
HitResult.ComboBreak,
HitResult.IgnoreMiss,
};
}
@@ -7,7 +7,7 @@ using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Configuration;
@@ -31,47 +31,45 @@ namespace osu.Game.Rulesets.Mania
Children = new Drawable[]
{
new SettingsEnumDropdown<ManiaScrollingDirection>
new SettingsItemV2(new FormEnumDropdown<ManiaScrollingDirection>
{
LabelText = RulesetSettingsStrings.ScrollingDirection,
Caption = RulesetSettingsStrings.ScrollingDirection,
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
},
new SettingsSlider<double, ManiaScrollSlider>
}),
new SettingsItemV2(new FormSliderBar<double>
{
LabelText = RulesetSettingsStrings.ScrollSpeed,
Caption = RulesetSettingsStrings.ScrollSpeed,
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 1
},
new SettingsCheckbox
KeyboardStep = 1,
LabelFormat = v => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(v), v),
}),
new SettingsItemV2(new FormCheckBox
{
Caption = RulesetSettingsStrings.TimingBasedColouring,
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
})
{
Keywords = new[] { "color" },
LabelText = RulesetSettingsStrings.TimingBasedColouring,
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
},
};
Add(new SettingsCheckbox
Add(new SettingsItemV2(new FormCheckBox
{
LabelText = RulesetSettingsStrings.TouchOverlay,
Caption = RulesetSettingsStrings.TouchOverlay,
Current = config.GetBindable<bool>(ManiaRulesetSetting.TouchOverlay)
});
}));
if (RuntimeInfo.IsMobile)
{
Add(new SettingsEnumDropdown<ManiaMobileLayout>
Add(new SettingsItemV2(new FormEnumDropdown<ManiaMobileLayout>
{
LabelText = RulesetSettingsStrings.MobileLayout,
Caption = RulesetSettingsStrings.MobileLayout,
Current = config.GetBindable<ManiaMobileLayout>(ManiaRulesetSetting.MobileLayout),
#pragma warning disable CS0618 // Type or member is obsolete
Items = Enum.GetValues<ManiaMobileLayout>().Where(l => l != ManiaMobileLayout.LandscapeWithOverlay),
#pragma warning restore CS0618 // Type or member is obsolete
});
}));
}
}
private partial class ManiaScrollSlider : RoundedSliderBar<double>
{
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
}
}
}
@@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Mods
typeof(ManiaModFadeIn)
}).ToArray();
public override bool Ranked => false;
public override bool Ranked => true;
public override bool ValidForFreestyleAsRequiredMod => false;
@@ -7,5 +7,15 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModDifficultyAdjust : ModDifficultyAdjust
{
public override DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 10,
// Use larger extended limits for mania to include OD values that occur with EZ or HR enabled
ExtendedMaxValue = 15,
ExtendedMinValue = -15,
ReadCurrentFromDifficulty = diff => diff.OverallDifficulty,
};
}
}
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Mania.Mods
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
double mostCommonBeatLengthBefore = beatmap.GetMostCommonBeatLength();
var newObjects = new List<ManiaHitObject>();
foreach (var h in beatmap.HitObjects.OfType<HoldNote>())
@@ -48,6 +51,17 @@ namespace osu.Game.Rulesets.Mania.Mods
}
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
double mostCommonBeatLengthAfter = beatmap.GetMostCommonBeatLength();
// the process of removing hold notes can result in shortening the beatmap's play time,
// and therefore, as a side effect, changing the most common BPM, which will change scroll speed.
// to compensate for this, apply a multiplier to effect points in order to maintain the beatmap's original intended scroll speed.
if (!Precision.AlmostEquals(mostCommonBeatLengthBefore, mostCommonBeatLengthAfter))
{
foreach (var effectPoint in beatmap.ControlPointInfo.EffectPoints)
effectPoint.ScrollSpeed *= mostCommonBeatLengthBefore / mostCommonBeatLengthAfter;
}
}
}
}
@@ -31,10 +31,21 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
public override bool DisplayResult => false;
/// <summary>
/// Whether the user is currently pressing the hold note.
/// </summary>
public IBindable<bool> IsHolding => isHolding;
private readonly Bindable<bool> isHolding = new Bindable<bool>();
/// <summary>
/// The time at which the user starting missing the hold note.
/// This could be the time at which they missed the head, broke on the body, or missed the tail.
/// </summary>
public IBindable<double?> MissingStartTime => missingStartTime;
private readonly Bindable<double?> missingStartTime = new Bindable<double?>();
public DrawableHoldNoteHead Head => headContainer.Child;
public DrawableHoldNoteTail Tail => tailContainer.Child;
public DrawableHoldNoteBody Body => bodyContainer.Child;
@@ -197,11 +208,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public override void OnKilled()
{
base.OnKilled();
// flush the final state of holding on kill.
// this matters because some skin implementations like legacy skin
// insert drawables in the hierarchy that are not a child of this DHO
// (see `LegacyBodyPiece` and related machinations with `lightContainer` being added at column level)
isHolding.Value = Result.IsHolding(Time.Current);
missingStartTime.Value = null;
(bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
}
@@ -209,6 +222,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
base.Update();
if (Head.Judged && !Head.IsHit)
missingStartTime.Value ??= Head.Result.TimeAbsolute;
if (Body.HasHoldBreak)
missingStartTime.Value ??= Body.Result.TimeAbsolute;
if (Tail.Judged && !Tail.IsHit)
missingStartTime.Value ??= Tail.Result.TimeAbsolute;
isHolding.Value = Result.IsHolding(Time.Current);
// Pad the full size container so its contents (i.e. the masking container) reach under the tail.
@@ -3,6 +3,7 @@
#nullable disable
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects.Drawables;
@@ -14,6 +15,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public partial class DrawableHoldNoteHead : DrawableNote
{
/// <summary>
/// The time at which the user starting missing the hold note.
/// This could be the time at which they missed the head, broke on the body, or missed the tail.
/// </summary>
public readonly IBindable<double?> MissingStartTime = new Bindable<double?>();
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteHead;
public DrawableHoldNoteHead()
@@ -28,6 +35,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Origin = Anchor.TopCentre;
}
protected override void OnApply()
{
base.OnApply();
if (ParentHitObject is DrawableHoldNote parentHold)
MissingStartTime.BindTo(parentHold.MissingStartTime);
}
protected override void OnFree()
{
base.OnFree();
if (ParentHitObject is DrawableHoldNote parentHold)
MissingStartTime.UnbindFrom(parentHold.MissingStartTime);
}
public bool UpdateResult() => base.UpdateResult(true);
protected override void UpdateHitStateTransforms(ArmedState state)
@@ -3,6 +3,7 @@
#nullable disable
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Scoring;
@@ -14,6 +15,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public partial class DrawableHoldNoteTail : DrawableNote
{
/// <summary>
/// The time at which the user starting missing the hold note.
/// This could be the time at which they missed the head, broke on the body, or missed the tail.
/// </summary>
public readonly IBindable<double?> MissingStartTime = new Bindable<double?>();
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
protected internal DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
@@ -30,6 +37,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Origin = Anchor.TopCentre;
}
protected override void OnApply()
{
base.OnApply();
if (ParentHitObject is DrawableHoldNote parentHold)
MissingStartTime.BindTo(parentHold.MissingStartTime);
}
protected override void OnFree()
{
base.OnFree();
if (ParentHitObject is DrawableHoldNote parentHold)
MissingStartTime.UnbindFrom(parentHold.MissingStartTime);
}
public void UpdateResult() => base.UpdateResult(true);
protected override void CheckForResult(bool userTriggered, double timeOffset) =>
+2 -1
View File
@@ -118,7 +118,8 @@ namespace osu.Game.Rulesets.Mania.Objects
AddNested(Body = new HoldNoteBody
{
StartTime = StartTime,
Column = Column
Column = Column,
Duration = Duration
});
}
@@ -3,6 +3,7 @@
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
@@ -13,9 +14,11 @@ namespace osu.Game.Rulesets.Mania.Objects
/// On hit - the hold note was held correctly for the full duration.<br />
/// On miss - the hold note was released at some point during its judgement period.
/// </summary>
public class HoldNoteBody : ManiaHitObject
public class HoldNoteBody : ManiaHitObject, IHasDuration
{
public override Judgement CreateJudgement() => new HoldNoteBodyJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public double Duration { get; set; }
public double EndTime => StartTime + Duration;
}
}
@@ -57,6 +57,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
if (spectatorList != null)
spectatorList.Position = new Vector2(36, -66);
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
new DrawableGameplayLeaderboard(),
@@ -25,17 +25,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly IBindable<bool> isHitting = new Bindable<bool>();
/// <summary>
/// Stores the start time of the fade animation that plays when any of the nested
/// hitobjects of the hold note are missed.
/// </summary>
private readonly Bindable<double?> missFadeTime = new Bindable<double?>();
private readonly IBindable<double?> missingStartTime = new Bindable<double?>();
private Drawable? bodySprite;
private Drawable? lightContainer;
private Drawable? light;
private LegacyNoteBodyStyle? bodyStyle;
@@ -87,6 +80,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHolding);
missingStartTime.BindTo(holdNote.MissingStartTime);
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30)?.With(d =>
{
@@ -109,26 +103,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
direction.BindValueChanged(onDirectionChanged, true);
isHitting.BindValueChanged(onIsHittingChanged, true);
missFadeTime.BindValueChanged(onMissFadeTimeChanged, true);
missingStartTime.BindValueChanged(onMissingStartTimeChanged, true);
holdNote.ApplyCustomUpdateState += applyCustomUpdateState;
applyCustomUpdateState(holdNote, holdNote.State.Value);
}
private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
{
switch (hitObject)
{
// Ensure that the hold note is also faded out when the head/tail/body is missed.
// Importantly, we filter out unrelated objects like DrawableNotePerfectBonus.
case DrawableHoldNoteTail:
case DrawableHoldNoteHead:
case DrawableHoldNoteBody:
if (state == ArmedState.Miss)
missFadeTime.Value ??= hitObject.HitStateUpdateTime;
break;
}
holdNote.ApplyCustomUpdateState += onApplyCustomUpdateState;
}
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
@@ -187,23 +164,19 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
}
}
private void onMissFadeTimeChanged(ValueChangedEvent<double?> missFadeTimeChange)
private void onMissingStartTimeChanged(ValueChangedEvent<double?> startTime)
=> applyMissingDim();
private void onApplyCustomUpdateState(DrawableHitObject obj, ArmedState state)
=> applyMissingDim();
private void applyMissingDim()
{
if (missFadeTimeChange.NewValue == null)
if (missingStartTime.Value == null)
return;
// this update could come from any nested object of the hold note (or even from an input).
// make sure the transforms are consistent across all affected parts.
using (BeginAbsoluteSequence(missFadeTimeChange.NewValue.Value))
{
// colour and duration matches stable
// transforms not applied to entire hold note in order to not affect hit lighting
const double fade_duration = 60;
holdNote.Head.FadeColour(Colour4.DarkGray, fade_duration);
holdNote.Tail.FadeColour(Colour4.DarkGray, fade_duration);
bodySprite?.FadeColour(Colour4.DarkGray, fade_duration);
}
using (BeginAbsoluteSequence(missingStartTime.Value.Value))
this.FadeColour(Colour4.DarkGray, 60);
}
protected override void Update()
@@ -213,9 +186,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (!isHitting.Value)
(bodySprite as TextureAnimation)?.GotoFrame(0);
if (holdNote.Body.HasHoldBreak)
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
// here we go...
@@ -251,7 +221,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
base.Dispose(isDisposing);
if (holdNote.IsNotNull())
holdNote.ApplyCustomUpdateState -= applyCustomUpdateState;
holdNote.ApplyCustomUpdateState -= onApplyCustomUpdateState;
lightContainer?.Expire();
}
@@ -1,18 +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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public partial class LegacyHoldNoteHeadPiece : LegacyNotePiece
{
private readonly IBindable<double?> missingStartTime = new Bindable<double?>();
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
missingStartTime.BindTo(((DrawableHoldNoteHead)drawableObject).MissingStartTime);
missingStartTime.BindValueChanged(onMissingStartTimeChanged, true);
drawableObject.ApplyCustomUpdateState += onApplyCustomUpdateState;
}
private void onMissingStartTimeChanged(ValueChangedEvent<double?> startTime)
=> applyMissingDim();
private void onApplyCustomUpdateState(DrawableHitObject obj, ArmedState state)
=> applyMissingDim();
private void applyMissingDim()
{
if (missingStartTime.Value == null)
return;
using (BeginAbsoluteSequence(missingStartTime.Value.Value))
this.FadeColour(Colour4.DarkGray, 60);
}
protected override Drawable? GetAnimation(ISkinSource skin)
{
// TODO: Should fallback to the head from default legacy skin instead of note.
return GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
?? GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject.IsNotNull())
drawableObject.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
}
}
@@ -1,8 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -10,6 +14,36 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public partial class LegacyHoldNoteTailPiece : LegacyNotePiece
{
private readonly IBindable<double?> missingStartTime = new Bindable<double?>();
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
missingStartTime.BindTo(((DrawableHoldNoteTail)drawableObject).MissingStartTime);
missingStartTime.BindValueChanged(onMissingStartTimeChanged, true);
drawableObject.ApplyCustomUpdateState += onApplyCustomUpdateState;
}
private void onMissingStartTimeChanged(ValueChangedEvent<double?> startTime)
=> applyMissingDim();
private void onApplyCustomUpdateState(DrawableHitObject obj, ArmedState state)
=> applyMissingDim();
private void applyMissingDim()
{
if (missingStartTime.Value == null)
return;
using (BeginAbsoluteSequence(missingStartTime.Value.Value))
this.FadeColour(Colour4.DarkGray, 60);
}
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
// Invert the direction
@@ -25,5 +59,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
?? GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.HoldNoteHeadImage)
?? GetAnimationFromLookup(skin, LegacyManiaSkinConfigurationLookups.NoteImage);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject.IsNotNull())
drawableObject.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
}
}
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable noteAnimation = null!;
private float? minimumColumnWidth;
private float? widthForNoteHeightScale;
public LegacyNotePiece()
{
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
minimumColumnWidth = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.MinimumColumnWidth))?.Value;
widthForNoteHeightScale = skin.GetConfig<ManiaSkinConfigurationLookup, float>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.WidthForNoteHeightScale))?.Value;
InternalChild = directionContainer = new Container
{
@@ -60,9 +60,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (texture != null)
{
// The height is scaled to the minimum column width, if provided.
float minimumWidth = minimumColumnWidth ?? DrawWidth;
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, minimumWidth), texture.DisplayWidth);
float noteHeight = widthForNoteHeightScale ?? DrawWidth;
noteAnimation.Scale = Vector2.Divide(new Vector2(DrawWidth, noteHeight), texture.DisplayWidth);
}
}
@@ -64,11 +64,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private readonly Lazy<bool> hasKeyTexture;
private readonly ManiaBeatmap beatmap;
private readonly bool isBeatmapConverted;
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
: base(skin)
{
this.beatmap = (ManiaBeatmap)beatmap;
isBeatmapConverted = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
hasKeyTexture = new Lazy<bool>(() =>
@@ -120,6 +122,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
leaderboard.Origin = Anchor.CentreLeft;
leaderboard.X = 10;
}
foreach (var d in container.OfType<ISerialisableDrawable>())
d.UsesFixedAnchor = true;
})
{
new LegacyManiaComboCounter(),
@@ -196,8 +201,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
public override ISample GetSample(ISampleInfo sampleInfo)
{
// layered hit sounds never play in mania
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
// layered hit sounds never play in mania-native beatmaps (but do play on converts)
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered && !isBeatmapConverted)
return new SampleVirtual();
return base.GetSample(sampleInfo);
@@ -9,9 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers;
@@ -63,16 +61,11 @@ namespace osu.Game.Rulesets.Mania.UI
public double TargetTimeRange { get; protected set; }
private double currentTimeRange;
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
private ISkinSource currentSkin = null!;
[Resolved]
private GameHost gameHost { get; set; } = null!;
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
: base(ruleset, beatmap, mods)
{
@@ -119,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.UI
TargetTimeRange = ComputeScrollTime(speed.NewValue);
});
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
TimeRange.Value = TargetTimeRange = ComputeScrollTime(configScrollSpeed.Value);
Config.BindWith(ManiaRulesetSetting.MobileLayout, mobileLayout);
mobileLayout.BindValueChanged(_ => updateMobileLayout(), true);
@@ -179,9 +172,7 @@ namespace osu.Game.Rulesets.Mania.UI
// This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position.
float scale = lengthToHitPosition / length_to_default_hit_position;
// we're intentionally using the game host's update clock here to decouple the time range tween from the gameplay clock (which can be arbitrarily paused, or even rewinding)
currentTimeRange = Interpolation.DampContinuously(currentTimeRange, TargetTimeRange, 50, gameHost.UpdateThread.Clock.ElapsedFrameTime);
TimeRange.Value = currentTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
TimeRange.Value = TargetTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
}
/// <summary>
+1 -1
View File
@@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Mania.UI
var hitWindows = new ManiaHitWindows();
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))));
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(hitWindows.IsHitResultAllowed)));
RegisterPool<BarLine, DrawableBarLine>(50, 200);
}
+1 -3
View File
@@ -35,11 +35,9 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
@@ -245,13 +245,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("grid spacing is distance to slider tail", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1)
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y);
});
AddAssert("grid rotation points to slider tail", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1);
});
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
@@ -280,9 +280,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("grid spacing and rotation unchanged", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1)
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y)
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1);
});
}
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
@@ -22,7 +23,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[TestFixture]
public partial class TestSceneSliderDrawing : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new TestBeatmap(ruleset, false);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
return beatmap;
}
[Test]
public void TestTouchInputPlaceHitCircleDirectly()
@@ -3,10 +3,13 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
@@ -30,6 +33,16 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
PathType.LINEAR,
new Vector2(100, 0),
new Vector2(100, 100)
),
createPathSegment(
PathType.PERFECT_CURVE,
new Vector2(100.009f, -50.0009f),
new Vector2(200.0089f, -100)
),
createPathSegment(
PathType.PERFECT_CURVE,
new Vector2(25, -50),
new Vector2(100, 75)
)
};
@@ -48,15 +61,19 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[TestCase(0, 250)]
[TestCase(0, 200)]
[TestCase(1, 120)]
[TestCase(1, 80)]
public void TestSliderReversal(int pathIndex, double length)
[TestCase(1, 120, false, false)]
[TestCase(1, 80, false, false)]
[TestCase(2, 250)]
[TestCase(2, 190)]
[TestCase(3, 250)]
[TestCase(3, 190)]
public void TestSliderReversal(int pathIndex, double length, bool assertEqualDistances = true, bool assertSliderReduction = true)
{
var controlPoints = paths[pathIndex];
Vector2 oldStartPos = default;
Vector2 oldEndPos = default;
double oldDistance = default;
double oldDistance = 0;
var oldControlPointTypes = controlPoints.Select(p => p.Type);
AddStep("Add slider", () =>
@@ -90,6 +107,215 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
InputManager.ReleaseKey(Key.LControl);
});
if (pathIndex == 2)
{
AddRepeatStep("Reverse slider again", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
}, 2);
}
if (assertEqualDistances)
{
AddAssert("Middle control point has the same distance from start to end", () =>
{
var pathControlPoints = selectedSlider.Path.ControlPoints;
float middleToStart = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[0].Position);
float middleToEnd = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^1].Position);
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
});
}
AddAssert("Middle control point is not at start or end", () =>
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldStartPos) > 1 &&
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldEndPos) > 1
);
AddAssert("Slider has correct length", () =>
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
AddAssert("Slider has correct start position", () =>
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
AddAssert("Slider has correct end position", () =>
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
AddAssert("Control points have correct types", () =>
{
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
});
if (assertSliderReduction)
{
AddStep("Move to marker", () =>
{
var marker = this.ChildrenOfType<SliderEndDragMarker>().Single();
var markerPos = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2;
// sometimes the cursor may miss the marker's hitbox so we
// add a little offset here to be sure it lands in a clickable position.
var position = new Vector2(markerPos.X + 2f, markerPos.Y);
InputManager.MoveMouseTo(position);
});
AddStep("Click", () => InputManager.PressButton(MouseButton.Left));
AddStep("Reduce slider", () =>
{
var middleControlPoint = this.ChildrenOfType<PathControlPointPiece<Slider>>().ToArray()[^2];
InputManager.MoveMouseTo(middleControlPoint);
});
AddStep("Release click", () => InputManager.ReleaseButton(MouseButton.Left));
AddStep("Save half slider info", () =>
{
oldStartPos = selectedSlider.Position;
oldEndPos = selectedSlider.EndPosition;
oldDistance = selectedSlider.Path.Distance;
});
AddStep("Reverse slider", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("Middle control point has the same distance from start to end", () =>
{
var pathControlPoints = selectedSlider.Path.ControlPoints;
float middleToStart = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[0].Position);
float middleToEnd = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^1].Position);
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
});
AddAssert("Middle control point is not at start or end", () =>
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldStartPos) > 1 &&
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, oldEndPos) > 1
);
AddAssert("Slider has correct length", () =>
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
AddAssert("Slider has correct start position", () =>
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
AddAssert("Slider has correct end position", () =>
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
AddAssert("Control points have correct types", () =>
{
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
});
}
}
[Test]
public void TestSegmentedSliderReversal()
{
PathControlPoint[] segmentedSliderPath =
[
new PathControlPoint
{
Position = new Vector2(0, 0),
Type = PathType.PERFECT_CURVE
},
new PathControlPoint
{
Position = new Vector2(100, 150),
},
new PathControlPoint
{
Position = new Vector2(75, -50),
Type = PathType.PERFECT_CURVE
},
new PathControlPoint
{
Position = new Vector2(225, -75),
},
new PathControlPoint
{
Position = new Vector2(350, 50),
Type = PathType.PERFECT_CURVE
},
new PathControlPoint
{
Position = new Vector2(500, -75),
},
new PathControlPoint
{
Position = new Vector2(350, -120),
},
];
Vector2 oldStartPos = default;
Vector2 oldEndPos = default;
double oldDistance = 0;
var oldControlPointTypes = segmentedSliderPath.Select(p => p.Type);
AddStep("Add slider", () =>
{
var slider = new Slider
{
Position = new Vector2(0, 200),
Path = new SliderPath(segmentedSliderPath)
{
ExpectedDistance = { Value = 1314 }
}
};
EditorBeatmap.Add(slider);
oldStartPos = slider.Position;
oldEndPos = slider.EndPosition;
oldDistance = slider.Path.Distance;
});
AddStep("Select slider", () =>
{
var slider = (Slider)EditorBeatmap.HitObjects[0];
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddRepeatStep("Reverse slider", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
}, 3);
AddAssert("First arc's control is not at the slider's middle", () =>
Vector2.Distance(selectedSlider.Path.ControlPoints[^2].Position, selectedSlider.Path.PositionAt(0.5)) > 1
);
AddAssert("Last arc's control is not at the slider's middle", () =>
Vector2.Distance(selectedSlider.Path.ControlPoints[1].Position, selectedSlider.Path.PositionAt(0.5)) > 1
);
AddAssert("First arc centered middle control point", () =>
{
var pathControlPoints = selectedSlider.Path.ControlPoints;
float middleToStart = Vector2.Distance(pathControlPoints[1].Position, pathControlPoints[0].Position);
float middleToEnd = Vector2.Distance(pathControlPoints[1].Position, pathControlPoints[2].Position);
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
});
AddAssert("Last arc centered middle control point", () =>
{
var pathControlPoints = selectedSlider.Path.ControlPoints;
float middleToStart = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^3].Position);
float middleToEnd = Vector2.Distance(pathControlPoints[^2].Position, pathControlPoints[^1].Position);
return Precision.AlmostEquals(middleToStart, middleToEnd, 1f);
});
AddAssert("Slider has correct length", () =>
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
@@ -6,6 +6,7 @@
using System;
using System.Linq;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Framework.Utils;
@@ -102,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
for (int i = 0; i < 100; i++)
{
Assert.True(Precision.AlmostEquals(sliderPathPerfect.PositionAt(i / 100.0f), sliderPathBezier.PositionAt(i / 100.0f)));
ClassicAssert.True(Precision.AlmostEquals(sliderPathPerfect.PositionAt(i / 100.0f), sliderPathBezier.PositionAt(i / 100.0f)));
}
}
@@ -174,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
double theta = circularArcProperties.ThetaStart + (circularArcProperties.Direction * progress * circularArcProperties.ThetaRange);
Vector2 vector = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * circularArcProperties.Radius;
Assert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f),
ClassicAssert.True(Precision.AlmostEquals(circularArcProperties.Centre + vector, path.PositionAt(progress), 0.01f),
"A perfect circle with points " + string.Join(", ", path.ControlPoints.Select(x => x.Position)) + " and radius" + circularArcProperties.Radius + "from SliderPath does not almost equal a theoretical perfect circle with " + subpoints + " subpoints"
+ ": " + (circularArcProperties.Centre + vector) + " - " + path.PositionAt(progress)
+ " = " + (circularArcProperties.Centre + vector - path.PositionAt(progress))
@@ -0,0 +1,88 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModEasy : OsuModTestScene
{
protected override bool AllowFail => true;
[Test]
public void TestMultipleApplication()
{
bool reapplied = false;
CreateModTest(new ModTestData
{
Mods = [new OsuModEasy { Retries = { Value = 1 } }],
Autoplay = false,
CreateBeatmap = () =>
{
// do stuff to speed up fails
var b = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
Difficulty = { DrainRate = 10 }
};
foreach (var ho in b.HitObjects)
ho.StartTime /= 4;
return b;
},
PassCondition = () =>
{
if (((ModEasyTestPlayer)Player).FailuresSuppressed > 0 && !reapplied)
{
try
{
foreach (var mod in Player.GameplayState.Mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(new BeatmapDifficulty());
foreach (var mod in Player.GameplayState.Mods.OfType<IApplicableToPlayer>())
mod.ApplyToPlayer(Player);
}
catch
{
// don't care if this fails. in fact a failure here is probably better than the alternative.
}
finally
{
reapplied = true;
}
}
return Player.GameplayState.HasFailed && ((ModEasyTestPlayer)Player).FailuresSuppressed <= 1;
}
});
}
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModEasyTestPlayer(CurrentTestData, AllowFail);
private partial class ModEasyTestPlayer : ModTestPlayer
{
public int FailuresSuppressed { get; private set; }
public ModEasyTestPlayer(ModTestData data, bool allowFail)
: base(data, allowFail)
{
}
protected override bool CheckModsAllowFailure()
{
bool failureAllowed = GameplayState.Mods.OfType<IApplicableFailOverride>().All(m => m.PerformFail());
if (!failureAllowed)
FailuresSuppressed++;
return failureAllowed;
}
}
}
}
@@ -2,7 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
@@ -18,5 +21,39 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
Autoplay = false,
});
}
[Test]
public void TestSkipToFirstCircleNotSuppressed()
{
CreateModTest(new ModTestData
{
Mod = new OsuModFreezeFrame(),
CreateBeatmap = () => new OsuBeatmap
{
HitObjects =
{
new HitCircle { StartTime = 5000, Position = OsuPlayfield.BASE_SIZE / 2 }
}
},
PassCondition = () => Player.GameplayClockContainer.GameplayStartTime > 0
});
}
[Test]
public void TestSkipToFirstSpinnerNotSuppressed()
{
CreateModTest(new ModTestData
{
Mod = new OsuModFreezeFrame(),
CreateBeatmap = () => new OsuBeatmap
{
HitObjects =
{
new Spinner { StartTime = 5000, Position = OsuPlayfield.BASE_SIZE / 2 }
}
},
PassCondition = () => Player.GameplayClockContainer.GameplayStartTime > 0
});
}
}
}
@@ -27,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestCase("multi-segment-slider")]
[TestCase("nan-slider")]
[TestCase("1124896")]
[TestCase("1341554")]
[TestCase("2593923")]
[TestCase("801165")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
[TestCase(0.14143808967817237d, 2, "nan-slider")]
[TestCase(6.6232533278125061d, 239, "diffcalc-test")]
[TestCase(1.5045783545699611d, 54, "zero-length-sliders")]
[TestCase(0.43333836671191595d, 4, "very-fast-slider")]
[TestCase(0.13841532030395723d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(9.6779746353001634d, 239, "diffcalc-test")]
[TestCase(1.7691451263718989d, 54, "zero-length-sliders")]
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
[TestCase(9.6491691624112761d, 239, "diffcalc-test")]
[TestCase(1.756936832498702d, 54, "zero-length-sliders")]
[TestCase(0.57771197086735004d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
[TestCase(6.6232533278125061d, 239, "diffcalc-test")]
[TestCase(1.5045783545699611d, 54, "zero-length-sliders")]
[TestCase(0.43333836671191595d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
File diff suppressed because one or more lines are too long
@@ -0,0 +1,941 @@
osu file format v14
[General]
AudioLeadIn: 0
PreviewTime: 76429
Countdown: 0
SampleSet: Soft
StackLeniency: 0.2
Mode: 0
LetterboxInBreaks: 0
WidescreenStoryboard: 1
[Difficulty]
HPDrainRate:5
CircleSize:4.3
OverallDifficulty:8
ApproachRate:9.3
SliderMultiplier:2.99999995231628
SliderTickRate:1
[Events]
//Background and Video events
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Sound Samples
[TimingPoints]
763,444.444444444444,4,2,1,60,1,0
763,-111.111111111111,4,2,1,60,0,0
1929,-100,4,2,1,5,0,0
1985,-100,4,2,1,60,0,0
2040,-100,4,2,1,5,0,0
2096,-153.846153846153,4,2,1,60,0,0
2429,-133.333333333333,4,2,1,5,0,0
2540,-71.4285714285714,4,2,1,70,0,1
2985,-100,4,2,1,70,0,1
4485,-100,4,2,1,5,0,1
4540,-100,4,2,1,70,0,1
4707,-100,4,2,1,5,0,1
4762,-100,4,2,1,70,0,1
4929,-100,4,2,1,5,0,1
4985,-100,4,2,1,70,0,1
5096,-83.3333333333333,4,2,1,70,0,1
5429,-133.333333333333,4,2,1,70,0,1
5596,-133.333333333333,4,2,1,70,0,1
5652,-133.333333333333,4,2,1,70,0,1
5818,-133.333333333333,4,2,1,70,0,1
5874,-133.333333333333,4,2,1,70,0,1
6040,-133.333333333333,4,2,1,70,0,1
6096,-100,4,2,1,70,0,1
6540,-100,4,2,1,70,0,1
8040,-100,4,2,1,5,0,1
8096,-100,4,2,1,70,0,1
8262,-100,4,2,1,5,0,1
8318,-100,4,2,1,70,0,1
8485,-100,4,2,1,5,0,1
8540,-133.333333333333,4,2,1,70,0,1
8874,-100,4,2,1,5,0,1
8985,-100,4,2,1,70,0,1
9651,-100,4,2,1,70,0,1
10096,-100,4,2,1,70,0,1
11596,-100,4,2,1,5,0,1
11651,-100,4,2,1,70,0,1
11818,-100,4,2,1,5,0,1
11873,-100,4,2,1,70,0,1
11874,-80,4,2,1,70,0,1
12040,-80,4,2,1,5,0,1
12096,-80,4,2,1,70,0,1
12207,-133.333333333333,4,2,1,70,0,1
12429,-100,4,2,1,5,0,1
12540,-100,4,2,1,70,0,1
12707,-100,4,2,1,70,0,1
12763,-100,4,2,1,70,0,1
12929,-100,4,2,1,70,0,1
12985,-100,4,2,1,70,0,1
13429,-300,4,2,1,70,0,1
13651,-83.3333333333333,4,2,1,70,0,1
13874,-100,4,2,1,70,0,1
15151,-100,4,2,1,5,0,1
15207,-100,4,2,1,70,0,1
15373,-100,4,2,1,5,0,1
15429,-100,4,2,1,70,0,1
15596,-100,4,2,1,5,0,1
15651,-100,4,2,1,70,0,1
15985,-100,4,2,1,5,0,1
16096,-100,4,2,1,70,0,1
16262,-100,4,2,1,70,0,1
16318,-83.3333333333333,4,2,1,70,0,1
16651,-100,4,2,1,70,0,1
16762,-133.333333333333,4,2,1,60,0,0
17096,-133.333333333333,4,2,1,5,0,0
17207,-200,4,2,1,60,0,0
18096,-66.6666666666667,4,2,1,60,0,0
18262,-66.6666666666667,4,2,1,5,0,0
18318,-66.6666666666667,4,2,1,60,0,0
18540,-100,4,2,1,60,0,0
18874,-100,4,2,1,5,0,0
18985,-100,4,2,1,60,0,0
19985,-100,4,2,1,60,0,0
20485,-100,4,2,1,5,0,0
20540,-100,4,2,1,60,0,0
20707,-100,4,2,1,5,0,0
20762,-200,4,2,1,60,0,0
20985,-100,4,2,1,60,0,0
21095,-100,4,2,1,60,0,0
21374,-100,4,2,1,5,0,0
21429,-100,4,2,1,60,0,0
21596,-100,4,2,1,5,0,0
21651,-100,4,2,1,60,0,0
21818,-100,4,2,1,5,0,0
21874,-66.6666666666667,4,2,1,60,0,0
21985,-100,4,2,1,60,0,0
22096,-100,4,2,1,60,0,0
22985,-200,4,2,1,60,0,0
23318,-100,4,2,1,60,0,0
23429,-100,4,2,1,60,0,0
23540,-100,4,2,1,60,0,0
23651,-100,4,2,1,60,0,0
23762,-100,4,2,1,60,0,0
23874,-133.333333333333,4,2,1,60,0,0
24208,-133.333333333333,4,2,1,5,0,0
24318,-200,4,2,1,5,0,0
24319,-200,4,2,1,60,0,0
24540,-100,4,2,1,60,0,0
24651,-66.6666666666667,4,2,1,60,0,0
24874,-100,4,2,1,60,0,0
25374,-100,4,2,1,5,0,0
25429,-100,4,2,1,60,0,0
27096,-100,4,2,1,60,0,0
27596,-100,4,2,1,5,0,0
27651,-100,4,2,1,60,0,0
27818,-100,4,2,1,5,0,0
27873,-133.333333333333,4,2,1,60,0,0
28096,-100,4,2,1,60,0,0
28206,-100,4,2,1,60,0,0
28485,-100,4,2,1,5,0,0
28540,-100,4,2,1,60,0,0
28707,-100,4,2,1,5,0,0
28762,-100,4,2,1,60,0,0
28929,-100,4,2,1,5,0,0
28985,-66.6666666666667,4,2,1,60,0,0
29151,-100,4,2,1,60,0,0
29207,-100,4,2,1,60,0,0
29651,-100,4,2,1,60,0,0
30429,-100,4,2,1,60,0,0
30540,-58.8235294117647,4,2,1,60,0,0
30874,-58.8235294117647,4,2,1,5,0,0
30985,-58.8235294117647,4,2,1,60,0,0
31040,-100,4,2,1,5,0,0
31429,-100,4,2,1,60,0,0
32485,-100,4,2,1,60,0,0
32540,-100,4,2,1,60,0,0
32707,-100,4,2,1,60,0,0
32762,-100,4,2,1,60,0,0
32985,-100,4,2,1,60,0,0
34318,-50,4,2,1,60,0,0
34485,-100,4,2,1,5,0,0
34540,-100,4,2,1,60,0,0
35151,-100,4,2,1,5,0,0
35207,-100,4,2,1,60,0,0
35374,-100,4,2,1,5,0,0
35430,-100,4,2,1,60,0,0
35818,-100,4,2,1,5,0,0
35874,-200,4,2,1,60,0,0
36429,-100,4,2,1,60,0,0
37818,-100,4,2,1,5,0,0
37874,-100,4,2,1,60,0,0
38040,-100,4,2,1,5,0,0
38096,-50,4,2,1,60,0,0
38151,-100,4,2,1,5,0,0
38540,-100,4,2,1,60,0,0
39596,-100,4,2,1,5,0,0
39651,-100,4,2,1,60,0,0
39818,-100,4,2,1,60,0,0
39873,-100,4,2,1,60,0,0
40096,-100,4,2,1,60,0,0
41429,-50,4,2,1,60,0,0
41596,-100,4,2,1,5,0,0
41651,-100,4,2,1,60,0,0
41818,-100,4,2,1,5,0,0
41874,-100,4,2,1,60,0,0
42040,-100,4,2,1,5,0,0
42096,-100,4,2,1,60,0,0
44318,-100,4,2,1,60,0,0
44762,-83.3333333333333,4,2,1,60,0,0
45207,-66.6666666666667,4,2,1,45,0,0
45651,-133.333333333333,4,2,1,45,0,0
51540,-133.333333333333,4,2,1,50,0,0
51651,-133.333333333333,4,2,1,45,0,0
52318,-133.333333333333,4,2,1,45,0,0
58540,-76.9230769230769,4,2,1,45,0,0
58818,-100,4,2,1,45,0,0
58874,-111.111111111111,4,2,1,45,0,0
59318,-111.111111111111,4,2,1,45,0,0
59429,-83.3333333333333,4,2,1,60,0,0
59540,-83.3333333333333,4,2,1,5,0,0
59874,-100,4,2,1,60,0,0
60096,-100,4,2,1,5,0,0
60207,-100,4,2,1,60,0,0
60707,-100,4,2,1,5,0,0
60763,-100,4,2,1,60,0,0
60818,-100,4,2,1,5,0,0
60874,-100,4,2,1,60,0,0
60929,-100,4,2,1,5,0,0
60985,-100,4,2,1,60,0,0
61040,-100,4,2,1,5,0,0
61096,-100,4,2,1,60,0,0
61151,-100,4,2,1,5,0,0
61207,-100,4,2,1,60,0,0
61596,-100,4,2,1,5,0,0
61651,-100,4,2,1,60,0,0
61762,-83.3333333333333,4,2,1,60,0,0
61985,-100,4,2,1,5,0,0
62096,-100,4,2,1,60,0,0
62151,-100,4,2,1,5,0,0
62207,-100,4,2,1,60,0,0
62262,-100,4,2,1,5,0,0
62318,-100,4,2,1,60,0,0
62374,-100,4,2,1,5,0,0
62430,-100,4,2,1,60,0,0
62485,-100,4,2,1,5,0,0
62540,-100,4,2,1,60,0,0
62596,-100,4,2,1,5,0,0
62651,-100,4,2,1,60,0,0
62707,-100,4,2,1,5,0,0
62762,-100,4,2,1,60,0,0
62818,-100,4,2,1,5,0,0
62874,-100,4,2,1,60,0,0
62929,-100,4,2,1,60,0,0
62930,-100,4,2,1,5,0,0
62985,-100,4,2,1,60,0,0
63707,-100,4,2,1,5,0,0
63762,-100,4,2,1,60,0,0
64262,-100,4,2,1,5,0,0
64318,-100,4,2,1,60,0,0
64485,-100,4,2,1,5,0,0
64540,-100,4,2,1,60,0,0
64596,-100,4,2,1,5,0,0
64651,-100,4,2,1,60,0,0
64707,-100,4,2,1,5,0,0
64762,-71.4285714285714,4,2,1,60,0,0
64929,-71.4285714285714,4,2,1,5,0,0
64984,-133.333333333333,4,2,1,60,0,0
65151,-133.333333333333,4,2,1,5,0,0
65206,-71.4285714285714,4,2,1,60,0,0
65374,-71.4285714285714,4,2,1,5,0,0
65429,-133.333333333333,4,2,1,60,0,0
65596,-133.333333333333,4,2,1,5,0,0
65651,-100,4,2,1,60,0,0
66540,-66.6666666666667,4,2,1,60,0,0
66596,-66.6666666666667,4,2,1,5,0,0
66929,-100,4,2,1,5,0,0
66985,-200,4,2,1,60,0,0
67207,-200,4,2,1,5,0,0
67318,-100,4,2,1,60,0,0
67818,-100,4,2,1,5,0,0
67874,-100,4,2,1,60,0,0
67929,-100,4,2,1,5,0,0
67985,-100,4,2,1,60,0,0
68040,-100,4,2,1,5,0,0
68096,-100,4,2,1,60,0,0
68151,-100,4,2,1,5,0,0
68207,-100,4,2,1,60,0,0
68262,-100,4,2,1,5,0,0
68318,-100,4,2,1,60,0,0
68874,-83.3333333333333,4,2,1,60,0,0
69096,-100,4,2,1,60,0,0
69097,-100,4,2,1,5,0,0
69207,-100,4,2,1,60,0,0
69263,-100,4,2,1,5,0,0
69319,-100,4,2,1,60,0,0
69374,-100,4,2,1,5,0,0
69430,-100,4,2,1,60,0,0
69486,-100,4,2,1,5,0,0
69542,-100,4,2,1,60,0,0
69597,-100,4,2,1,5,0,0
69651,-100,4,2,1,60,0,0
69707,-100,4,2,1,5,0,0
69762,-100,4,2,1,60,0,0
69818,-100,4,2,1,5,0,0
69874,-100,4,2,1,60,0,0
69929,-100,4,2,1,5,0,0
69985,-100,4,2,1,60,0,0
70040,-100,4,2,1,60,0,0
70041,-100,4,2,1,5,0,0
70096,-100,4,2,1,60,0,0
70818,-100,4,2,1,5,0,0
70873,-100,4,2,1,60,0,0
71207,-71.4285714285714,4,2,1,60,0,0
71429,-100,4,2,1,60,0,0
71874,-71.4285714285714,4,2,1,60,0,0
72041,-71.4285714285714,4,2,1,5,0,0
72096,-133.333333333333,4,2,1,60,0,0
72263,-133.333333333333,4,2,1,5,0,0
72318,-71.4285714285714,4,2,1,60,0,0
72485,-71.4285714285714,4,2,1,5,0,0
72540,-133.333333333333,4,2,1,60,0,0
72985,-66.6666666666667,4,2,1,60,0,0
73207,-100,4,2,1,60,0,0
73651,-133.333333333333,4,2,1,45,0,0
75318,-133.333333333333,4,2,1,5,0,0
75429,-133.333333333333,4,2,1,45,0,0
76762,-100,4,2,1,45,0,0
77096,-100,4,2,1,5,0,0
77207,-100,4,2,1,70,0,1
77818,-100,4,2,1,5,0,1
77874,-100,4,2,1,70,0,1
78262,-100,4,2,1,5,0,1
78318,-100,4,2,1,70,0,1
78540,-83.3333333333333,4,2,1,70,0,1
78985,-100,4,2,1,70,0,1
79596,-100,4,2,1,5,0,1
79651,-100,4,2,1,70,0,1
80040,-100,4,2,1,5,0,1
80096,-100,4,2,1,70,0,1
80318,-83.3333333333333,4,2,1,70,0,1
84318,-100,4,2,1,70,0,1
84929,-100,4,2,1,5,0,1
84985,-100,4,2,1,70,0,1
85207,-100,4,2,1,70,0,1
85374,-100,4,2,1,5,0,1
85429,-100,4,2,1,70,0,1
85651,-83.3333333333333,4,2,1,70,0,1
86096,-100,4,2,1,70,0,1
86707,-100,4,2,1,5,0,1
86762,-100,4,2,1,70,0,1
88818,-100,4,2,1,5,0,1
88874,-100,4,2,1,70,0,1
88929,-100,4,2,1,5,0,1
88985,-100,4,2,1,70,0,1
89040,-100,4,2,1,5,0,1
89096,-100,4,2,1,70,0,1
92040,-100,4,2,1,5,0,1
92096,-100,4,2,1,70,0,1
92485,-100,4,2,1,5,0,1
92540,-100,4,2,1,70,0,1
97651,-200,4,2,1,70,0,1
97818,-200,4,2,1,5,0,1
97874,-66.6666666666667,4,2,1,70,0,1
97985,-66.6666666666667,4,2,1,70,0,1
98040,-66.6666666666667,4,2,1,5,0,1
98096,-133.333333333333,4,2,1,70,0,1
98262,-133.333333333333,4,2,1,5,0,1
98318,-66.6666666666667,4,2,1,70,0,1
98540,-100,4,2,1,70,0,1
99151,-100,4,2,1,5,0,1
99207,-100,4,2,1,70,0,1
99596,-100,4,2,1,5,0,1
99651,-100,4,2,1,70,0,1
103040,-100,4,2,1,5,0,1
103096,-100,4,2,1,70,0,1
103151,-100,4,2,1,5,0,1
103207,-100,4,2,1,70,0,1
103262,-100,4,2,1,5,0,1
103318,-100,4,2,1,70,0,1
105207,-83.3333333333333,4,2,1,70,0,1
105540,-83.3333333333333,4,2,1,70,0,1
105651,-133.333333333333,4,2,1,60,0,0
105985,-133.333333333333,4,2,1,5,0,0
106096,-200,4,2,1,60,0,0
106985,-66.6666666666667,4,2,1,60,0,0
107151,-66.6666666666667,4,2,1,5,0,0
107207,-66.6666666666667,4,2,1,60,0,0
107429,-100,4,2,1,60,0,0
107763,-100,4,2,1,5,0,0
107874,-100,4,2,1,60,0,0
108874,-100,4,2,1,60,0,0
109374,-100,4,2,1,5,0,0
109429,-100,4,2,1,60,0,0
109596,-100,4,2,1,5,0,0
109651,-200,4,2,1,60,0,0
109929,-100,4,2,1,60,0,0
109984,-100,4,2,1,60,0,0
110262,-100,4,2,1,5,0,0
110318,-100,4,2,1,60,0,0
110485,-100,4,2,1,5,0,0
110540,-100,4,2,1,60,0,0
110707,-100,4,2,1,5,0,0
110762,-66.6666666666667,4,2,1,60,0,0
110929,-100,4,2,1,60,0,0
110985,-133.333333333333,4,2,1,60,0,0
111429,-133.333333333333,4,2,1,60,0,0
111596,-133.333333333333,4,2,1,60,0,0
111651,-133.333333333333,4,2,1,60,0,0
111818,-133.333333333333,4,2,1,60,0,0
111874,-100,4,2,1,60,0,1
112318,-83.3333333333333,4,2,1,60,0,1
112429,-100,4,2,1,5,0,0
[Colours]
Combo1 : 112,75,180
Combo2 : 0,255,255
Combo3 : 255,15,117
Combo4 : 255,135,15
[HitObjects]
309,230,763,37,0,3:0:0:0:
485,146,985,2,0,L|406:167,1,67.4999968671799,8|0,3:0|0:0,0:0:0:0:
374,249,1207,2,0,L|299:227,1,67.4999968671799,8|0,3:0|0:0,0:0:0:0:
196,91,1429,2,0,L|191:44,3,33.7499984335899,0|0|0|0,3:0|3:0|3:0|3:0,0:0:0:0:
124,173,1651,2,0,L|131:222,2,44.9999979114532,0|0|0,3:0|3:0|3:0,0:0:0:0:
221,284,1874,2,0,L|213:208,1,67.4999968671799,0|0,3:0|3:0,0:0:0:0:
292,86,2096,38,0,L|310:234,1,146.249990980625,12|0,3:0|0:0,0:0:0:0:
314,328,2540,38,0,B|280:359|280:359|230:320|252:242|313:230,1,209.999990253448,0|0,3:0|0:0,0:0:0:0:
421,300,2874,1,0,0:0:0:0:
421,300,2985,2,0,P|461:288|491:253,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
309,231,3207,2,0,P|297:190|305:153,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
394,22,3429,5,0,3:0:0:0:
461,72,3540,2,0,B|477:103|477:103|461:148,1,74.999998807907,0|4,0:0|0:0,0:0:0:0:
378,183,3762,2,0,L|206:157,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
229,161,4096,2,0,P|227:202|211:250,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
61,384,4318,38,0,P|101:359|134:322,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
317,310,4540,2,0,P|267:305|226:288,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
141,110,4762,2,0,B|121:175|152:226|152:226|152:202|161:183,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
155,196,5096,6,0,P|67:211|79:286,1,179.999991645813,0|0,0:0|0:0,0:0:0:0:
212,366,5429,38,0,P|207:335|174:281,1,56.2500012516975,4|0,0:0|0:0,0:0:0:0:
206,286,5651,2,0,P|236:297|299:295,1,56.2500012516975,8|0,3:0|0:0,0:0:0:0:
281,321,5874,2,0,P|257:340|227:396,1,56.2500012516975,4|0,0:0|0:0,0:0:0:0:
124,246,6096,6,0,P|198:198|277:232,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
253,211,6429,1,0,0:0:0:0:
276,99,6540,2,0,P|335:139|369:215,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
368,208,6874,1,0,0:0:0:0:
430,96,6985,37,0,3:0:0:0:
497,147,7096,2,0,P|507:189|488:244,1,74.999998807907,0|4,0:0|0:0,0:0:0:0:
414,379,7318,2,0,B|383:322|421:267|421:267|421:308,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
421,298,7651,2,0,P|378:312|336:304,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
270,170,7874,6,0,P|275:228|236:278,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
94,300,8096,2,0,P|133:263|208:274,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
261,374,8318,2,0,L|176:365,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
38,377,8540,2,0,L|55:197,1,168.750003755093,4|0,0:0|0:0,0:0:0:0:
123,25,8985,38,0,L|132:110,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
217,242,9207,2,0,L|237:168,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
48,92,9429,5,4,0:0:0:0:
63,176,9540,1,0,0:0:0:0:
83,259,9651,38,0,P|167:223|231:255,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
274,312,9985,1,0,0:0:0:0:
274,312,10096,2,0,L|354:292,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
459,225,10318,2,0,L|375:204,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
269,107,10540,1,0,3:0:0:0:
276,54,10651,1,0,0:0:0:0:
313,17,10762,1,4,0:0:0:0:
363,9,10874,1,0,0:0:0:0:
363,9,11096,5,0,0:0:0:0:
432,68,11207,2,0,P|444:107|425:154,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
309,252,11429,38,0,P|297:195|321:158,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
450,316,11651,2,0,L|361:312,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
160,341,11874,2,0,B|187:380|187:380|233:309|177:235,1,187.499997019767,8|4,3:0|0:0,0:0:0:0:
116,200,12207,6,0,P|52:224|122:264,1,168.750003755093,0|4,0:0|0:0,0:0:0:0:
297,91,12762,37,8,3:0:0:0:
276,44,12874,1,0,0:0:0:0:
226,27,12985,1,4,0:0:0:0:
187,63,13096,1,0,0:0:0:0:
196,115,13207,1,0,0:0:0:0:
376,144,13429,2,0,L|378:121,2,16.6666664017571,0|0|0,0:0|0:0|0:0,0:0:0:0:
436,220,13651,6,0,B|395:211|373:164|373:164|332:208|264:185,1,179.999991645813,8|4,3:0|0:0,0:0:0:0:
276,44,13985,1,0,0:0:0:0:
196,115,14096,38,0,L|139:124,4,37.4999994039535,0|0|0|0|4,3:0|0:0|0:0|0:0|0:0,0:0:0:0:
82,69,14429,1,0,0:0:0:0:
106,190,14540,2,0,L|126:276,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
218,383,14762,2,0,L|234:309,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
26,231,14985,5,0,3:0:0:0:
253,202,15207,37,0,0:0:0:0:
331,271,15318,1,0,0:0:0:0:
233,309,15429,1,8,3:0:0:0:
389,73,15651,6,0,P|410:22|447:112,1,224.999996423721,4|0,3:0|0:0,0:0:0:0:
391,165,16096,1,0,0:0:0:0:
377,177,16207,1,0,0:0:0:0:
365,187,16318,38,0,B|253:261|221:119|94:192,1,269.999987468719,0|0,0:0|0:0,0:0:0:0:
73,319,16762,22,0,P|133:336|116:236,1,168.750003755093,4|0,3:0|0:0,0:0:0:0:
139,258,17207,6,0,P|138:315|69:283,1,112.500002503395,8|0,3:0|0:0,0:0:0:0:
92,323,17762,37,0,0:0:0:0:
43,245,17874,1,4,0:0:0:0:
4,322,17985,1,0,0:0:0:0:
133,245,18096,1,0,3:0:0:0:
29,105,18318,6,0,L|38:40,3,56.2500012516975,4|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
50,30,18540,38,0,P|111:56|193:25,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
240,120,18985,2,0,P|328:91|394:125,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
409,213,19318,2,0,B|377:226|377:226|243:200,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
119,187,19651,2,0,L|127:286,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
179,338,19874,1,8,3:0:0:0:
45,307,19985,6,0,L|3:297,2,37.4999994039535,0|0|4,0:0|0:0|0:0,0:0:0:0:
103,380,20207,1,0,3:0:0:0:
212,257,20318,38,0,P|233:218|231:171,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
111,118,20540,1,4,0:0:0:0:
111,118,20762,6,0,L|197:109,1,74.999998807907,8|4,3:0|0:0,0:0:0:0:
256,18,21096,37,0,0:0:0:0:
337,121,21207,2,0,P|350:60|403:16,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
384,26,21429,2,0,P|406:86|465:122,1,112.49999821186,0|0,0:0|0:0,0:0:0:0:
443,114,21651,2,0,P|377:105|327:131,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
352,223,21874,6,0,B|369:230|369:230|391:228|391:228|416:239|416:239|440:235|440:235|462:244|462:244|489:249,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
322,343,22096,37,0,3:0:0:0:
259,270,22207,2,0,P|223:276|182:263,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
86,360,22540,5,8,3:0:0:0:
15,295,22651,2,0,L|0:201,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
94,384,22985,38,0,P|118:328|112:277,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
0,211,23429,22,0,L|76:196,1,74.999998807907,12|0,3:0|0:0,0:0:0:0:
215,134,23651,2,0,L|114:110,1,74.999998807907,12|0,3:0|0:0,0:0:0:0:
33,124,23874,22,0,L|43:2,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
150,269,24318,2,0,L|162:194,1,74.999998807907,0|4,3:0|0:0,0:0:0:0:
229,134,24651,6,0,L|386:164,1,112.500002503395,12|0,3:0|0:0,0:0:0:0:
486,268,24874,37,0,0:0:0:0:
410,119,24985,1,4,0:0:0:0:
381,213,25096,1,0,0:0:0:0:
512,120,25207,1,0,3:0:0:0:
247,36,25429,6,0,L|191:25,3,37.4999994039535,4|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
185,24,25651,2,0,B|145:72|145:72|174:164,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
253,219,26096,2,0,B|281:311|281:311|228:382,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
100,363,26429,38,0,L|259:354,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
404,262,26762,1,4,0:0:0:0:
390,352,26874,1,0,0:0:0:0:
314,295,26985,1,8,3:0:0:0:
425,256,27096,6,0,L|492:246,2,37.4999994039535,0|0|4,0:0|0:0|0:0,0:0:0:0:
329,216,27318,1,0,3:0:0:0:
193,177,27429,38,0,L|266:161,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
322,107,27651,1,4,0:0:0:0:
322,107,27874,2,0,L|310:238,1,112.500002503395,8|4,3:0|0:0,0:0:0:0:
110,299,28207,5,0,0:0:0:0:
164,231,28318,2,0,B|168:303|168:303|121:338,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
30,284,28540,2,0,B|90:244|90:244|144:267,1,112.49999821186,0|0,0:0|0:0,0:0:0:0:
148,371,28762,2,0,B|83:338|83:338|76:280,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
194,201,28985,38,0,B|207:210|207:210|227:210|227:210|243:217|243:217|265:218|265:218|282:227|282:227|305:225|305:225|325:238,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
492,114,29207,6,0,P|445:136|410:138,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
324,102,29429,2,0,P|291:68|280:29,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
418,17,29651,1,8,3:0:0:0:
495,201,29874,1,4,3:2:0:0:
221,136,30096,37,0,3:0:0:0:
299,188,30207,2,0,B|316:251|316:251|271:352,1,149.999997615814,4|0,3:0|0:0,0:0:0:0:
115,334,30540,6,0,P|11:252|167:266,1,382.500001215934,0|0,0:0|0:0,0:0:0:0:
216,326,30985,38,0,L|304:331,1,63.7500002026557,4|0,3:0|0:0,0:0:0:0:
280,330,31429,6,0,L|293:241,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
426,252,31651,2,0,L|439:163,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
253,158,31874,37,0,3:0:0:0:
258,132,31985,1,0,0:0:0:0:
337,111,32096,5,4,0:0:0:0:
341,85,32207,1,0,0:0:0:0:
271,30,32318,38,0,B|212:42|212:42|141:19,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
163,26,32540,2,0,L|144:181,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
445,343,32985,22,0,B|439:234|439:234|384:269,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
240,257,33429,2,0,B|263:148|263:148|291:205,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
68,333,33874,2,0,B|83:233|83:233|41:256,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
344,347,34318,22,0,B|368:372|368:372|455:355|455:355|472:308,1,149.999997615814,4|0,0:0|0:0,0:0:0:0:
452,255,34540,2,0,B|389:212|389:212|332:273,1,149.999997615814,0|4,3:0|0:0,0:0:0:0:
256,220,34874,5,0,0:0:0:0:
256,220,34985,2,0,B|256:128,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
256,70,35207,2,0,B|256:162,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
112,312,35429,37,0,3:0:0:0:
60,255,35540,2,0,B|123:212|123:212|180:273,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
169,350,35874,6,0,B|144:375|144:375|57:358|57:358|40:311,1,149.999997615814,8|0,3:0|3:0,0:0:0:0:
62,169,36429,6,0,L|76:267,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
134,61,36762,1,8,3:0:0:0:
201,113,36874,2,0,L|215:211,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
298,272,37207,6,0,L|315:184,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
330,114,37429,1,4,0:0:0:0:
446,176,37540,2,0,B|404:214|404:214|307:197,1,149.999997615814,4|0,3:0|0:0,0:0:0:0:
231,240,37874,2,0,P|223:199|231:162,1,74.999998807907,4|0,3:0|0:0,0:0:0:0:
325,285,38096,6,0,L|154:300,1,149.999997615814,4|0,3:0|0:0,0:0:0:0:
175,298,38540,6,0,L|163:396,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
75,208,38762,2,0,L|63:306,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
233,74,38985,37,0,3:0:0:0:
231,98,39096,1,0,0:0:0:0:
156,139,39207,5,4,0:0:0:0:
155,165,39318,1,0,0:0:0:0:
227,215,39429,38,0,P|282:209|352:230,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
336,222,39651,2,0,L|366:67,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
81,35,40096,22,0,B|82:105|82:105|118:136|118:136|132:89,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
272,158,40540,2,0,B|270:228|270:228|234:259|234:259|220:212,1,149.999997615814,4|0,0:0|3:0,0:0:0:0:
423,36,40985,2,0,B|400:102|400:102|423:143|423:143|453:104,1,149.999997615814,4|8,0:0|3:0,0:0:0:0:
512,278,41429,6,0,P|415:258|361:293,1,149.999997615814,4|0,0:0|0:0,0:0:0:0:
359,302,41651,6,0,B|320:264|320:264|310:187,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
322,190,41874,2,0,L|449:171,1,112.49999821186,4|0,0:0|0:0,0:0:0:0:
443,159,42096,1,8,3:0:0:0:
240,52,42318,6,0,B|255:79|255:79|241:135,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
177,166,42540,1,0,3:0:0:0:
163,151,42651,2,0,B|161:207|161:207|192:240|192:240|189:299,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
131,365,42985,2,0,P|198:322|280:345,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
335,377,43318,1,0,0:0:0:0:
442,239,43429,38,0,B|456:178|456:178|422:136|422:136|427:68|427:68|449:112,1,224.999996423721,0|0,3:0|0:0,0:0:0:0:
444,103,43874,2,0,P|402:118|356:120,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
249,28,44096,2,0,P|295:35|324:48,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
364,201,44318,5,0,0:0:0:0:
332,195,44429,1,0,0:0:0:0:
251,135,44540,37,0,0:0:0:0:
281,123,44651,1,0,0:0:0:0:
332,195,44762,6,0,B|356:269|324:333|324:333|303:293,1,179.999991645813,4|0,0:3|0:0,0:0:0:0:
61,25,45207,38,0,L|88:158,1,112.500002503395,0|0,3:0|0:0,0:0:0:0:
84,136,45651,1,8,3:0:0:0:
84,136,46096,1,0,3:0:0:0:
176,33,46207,2,0,L|164:103,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
219,207,46429,2,0,L|232:152,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
312,65,46651,1,0,0:0:0:0:
312,65,46762,2,0,L|398:94,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
512,176,46985,5,0,3:0:0:0:
421,192,47096,1,0,0:0:0:0:
421,192,47429,1,8,3:0:0:0:
402,357,47651,37,0,0:0:0:0:
394,277,47762,1,0,0:0:0:0:
328,324,47874,1,0,3:0:0:0:
328,324,48318,1,8,3:0:0:0:
110,357,48540,5,0,0:0:0:0:
118,277,48651,1,0,0:0:0:0:
184,324,48763,1,0,3:0:0:0:
110,357,48874,1,0,0:0:0:0:
110,357,49207,1,8,3:0:0:0:
110,357,49651,1,0,3:0:0:0:
0,283,49762,38,0,P|41:301|97:295,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
188,219,49985,2,0,P|168:236|137:246,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
49,137,50207,1,0,0:0:0:0:
49,137,50318,2,0,P|65:184|93:205,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
107,67,50540,5,0,3:0:0:0:
32,15,50651,1,0,0:0:0:0:
32,15,50985,1,8,3:0:0:0:
265,114,51207,37,0,0:0:0:0:
254,196,51318,1,0,0:0:0:0:
241,279,51429,1,0,3:0:0:0:
241,279,51651,1,0,0:0:0:0:
336,207,51762,6,0,P|397:191|371:274,1,168.750003755093,0|0,0:0|0:0,0:0:0:0:
83,206,52318,5,0,3:0:0:0:
83,206,52429,2,0,L|101:260,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
40,383,52651,2,0,P|70:355|90:324,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
214,334,52874,1,0,0:0:0:0:
214,334,52985,2,0,P|171:322|140:304,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
151,160,53207,5,0,3:0:0:0:
188,135,53318,1,0,0:0:0:0:
232,129,53429,1,0,0:0:0:0:
273,146,53540,1,0,0:0:0:0:
339,198,53651,37,8,3:0:0:0:
383,199,53762,1,0,0:0:0:0:
426,185,53874,1,0,0:0:0:0:
450,147,53985,1,0,0:0:0:0:
444,61,54096,6,0,P|414:28|377:15,1,56.2500012516975,0|0,3:0|0:0,0:0:0:0:
301,28,54318,2,0,P|268:48|255:77,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
189,271,54540,38,0,P|209:222|204:198,1,56.2500012516975,8|0,3:0|0:0,0:0:0:0:
186,114,54762,2,0,P|152:74|124:68,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
27,137,54985,5,0,3:0:0:0:
34,167,55096,1,0,0:0:0:0:
122,204,55207,37,0,0:0:0:0:
116,178,55318,1,0,0:0:0:0:
48,249,55429,5,8,3:0:0:0:
54,274,55540,1,0,0:0:0:0:
124,329,55651,38,0,P|157:326|200:310,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
320,185,55874,5,0,3:0:0:0:
287,175,55985,1,0,0:0:0:0:
254,181,56096,2,0,P|258:221|264:241,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
337,347,56318,2,0,P|348:321|350:293,1,56.2500012516975,8|0,3:0|0:0,0:0:0:0:
418,197,56540,37,0,0:0:0:0:
418,197,56651,2,0,L|492:180,1,56.2500012516975,0|0,0:0|3:0,0:0:0:0:
329,114,56874,2,0,L|262:94,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
436,59,57096,6,0,L|413:126,1,56.2500012516975,0|8,0:0|3:0,0:0:0:0:
332,194,57318,2,0,L|353:259,2,56.2500012516975,0|0|0,0:0|0:0|0:0,0:0:0:0:
202,194,57651,37,0,3:0:0:0:
224,233,57762,1,0,0:0:0:0:
222,279,57874,1,0,0:0:0:0:
193,314,57985,1,0,0:0:0:0:
144,244,58096,5,0,0:0:0:0:
127,214,58207,1,0,0:0:0:0:
126,180,58318,1,0,0:0:0:0:
139,149,58429,1,0,0:0:0:0:
224,113,58540,38,0,B|262:88|235:70|189:83|189:83|224:138|194:193,1,194.999987974167
299,319,58874,1,0,0:0:0:0:
299,319,58985,2,0,B|316:283|314:237|314:237|278:226|278:226|320:243|359:227,1,202.49999060154,4|0,0:0|0:0,0:0:0:0:
428,181,59429,22,0,P|454:129|399:4,1,179.999991645813,0|0,3:0|0:0,0:0:0:0:
418,18,59874,2,0,L|373:15,6,24.9999996026357,8|0|0|0|0|0|4,3:0|0:0|0:0|0:0|0:0|0:0|0:0,0:0:0:0:
428,181,60207,5,0,0:0:0:0:
352,209,60318,1,0,3:0:0:0:
278,177,60429,1,0,0:0:0:0:
208,225,60540,2,0,L|222:267,2,37.4999994039535,4|0|0,0:0|0:0|0:0,0:0:0:0:
71,144,60762,38,0,L|65:109,3,24.9999996026357,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
145,86,60985,1,4,0:0:0:0:
163,127,61096,1,0,0:0:0:0:
161,171,61207,1,0,3:0:0:0:
136,208,61318,1,0,0:0:0:0:
99,231,61429,2,0,B|91:279|91:279|117:314,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
177,378,61651,5,8,3:0:0:0:
177,378,61762,2,0,B|231:371|231:371|272:326|272:326|345:319,1,179.999991645813
417,293,62096,1,0,3:0:0:0:
438,263,62207,1,0,0:0:0:0:
436,225,62318,1,4,0:0:0:0:
412,196,62429,1,0,0:0:0:0:
320,172,62540,6,0,P|307:192|291:204,1,37.4999994039535,8|0,3:0|0:0,0:0:0:0:
291,147,62651,2,0,P|274:156|245:153,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
276,114,62762,2,0,P|250:107|234:94,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
283,81,62874,2,0,P|265:61|260:45,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
365,31,62985,38,0,P|398:44|442:49,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
512,169,63207,2,0,P|466:163|421:176,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
350,107,63429,1,8,3:0:0:0:
293,237,63540,38,0,L|276:158,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
428,269,63762,2,0,B|373:275|373:275|338:249|338:249|267:255,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
191,318,64096,2,0,B|182:355|182:355|212:395,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
192,186,64318,5,8,3:0:0:0:
135,253,64429,2,0,L|56:270,2,74.999998807907,0|4|0,0:0|0:0|0:0,0:0:0:0:
24,136,64762,38,0,P|69:76|158:75,1,157.499992690086,0|0,3:0|0:0,0:0:0:0:
160,80,64985,6,0,P|193:102|255:102,1,84.3750018775463,4|0,0:0|0:0,0:0:0:0:
276,34,65207,38,0,L|290:212,1,157.499992690086,8|0,3:0|0:0,0:0:0:0:
291,219,65429,6,0,L|311:132,1,84.3750018775463,4|0,0:0|0:0,0:0:0:0:
381,111,65651,38,0,B|418:126|418:126|460:126,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
221,163,65874,2,0,B|186:143|186:143|139:153,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
41,231,66096,1,8,3:0:0:0:
49,267,66207,1,0,0:0:0:0:
56,303,66318,1,4,0:0:0:0:
67,288,66429,1,4,0:0:0:0:
77,270,66540,6,0,P|171:255|72:350,1,337.500007510185,0|0,0:0|0:0,0:0:0:0:
95,356,66985,38,0,L|185:343,1,74.999998807907,8|4,3:0|0:0,0:0:0:0:
274,286,67318,6,0,B|289:324|289:324|268:378,1,74.999998807907,0|0,0:0|3:0,0:0:0:0:
191,227,67540,1,0,0:0:0:0:
255,168,67651,2,0,L|264:116,2,37.4999994039535,4|0|0,0:0|0:0|0:0,0:0:0:0:
147,83,67874,2,0,L|154:108,3,24.9999996026357,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
80,148,68096,38,0,L|98:224,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
125,356,68318,1,0,3:0:0:0:
0,319,68429,1,0,0:0:0:0:
0,319,68540,2,0,L|76:294,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
277,219,68762,5,8,3:0:0:0:
277,219,68874,2,0,B|327:199|327:199|293:138|197:173,1,179.999991645813,0|0,0:0|0:0,0:0:0:0:
157,273,69207,37,0,3:0:0:0:
175,316,69318,1,0,0:0:0:0:
212,334,69429,1,4,0:0:0:0:
254,333,69540,1,0,0:0:0:0:
332,268,69651,38,0,P|333:237|343:213,1,37.4999994039535,8|0,3:0|0:0,0:0:0:0:
373,265,69762,2,0,P|386:239|404:232,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
413,284,69874,2,0,P|430:269|454:269,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
433,318,69985,2,0,P|452:320|474:337,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
401,384,70096,6,0,P|353:378|319:346,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
251,251,70318,2,0,P|240:196|260:154,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
401,18,70540,1,8,3:0:0:0:
401,18,70651,2,0,P|409:54|398:90,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
327,193,70874,2,0,L|304:45,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
290,26,71207,6,0,L|308:144,1,104.999995126724,0|0,0:0|0:0,0:0:0:0:
272,302,71429,2,0,L|187:288,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
33,217,71651,37,4,0:0:0:0:
27,187,71762,1,4,0:0:0:0:
20,157,71874,2,0,B|103:140|103:140|162:58,1,157.499992690086,0|0,3:0|0:0,0:0:0:0:
145,82,72096,6,0,L|218:75,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
336,136,72318,38,0,P|331:213|231:208,1,157.499992690086
263,232,72540,6,0,L|278:300,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
183,384,72762,2,0,L|172:307,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
37,140,72985,38,0,B|10:168|10:168|17:204|17:204|54:220|54:220|89:196|89:196|87:157|87:157|57:138,1,225.00000500679
275,372,73651,6,0,P|320:352|387:369,1,112.500002503395
380,364,74096,2,0,L|436:358,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
495,271,74318,2,0,L|424:282,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
339,270,74540,1,0,0:0:0:0:
339,270,74651,1,0,0:0:0:0:
339,270,74762,2,0,L|329:196,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
408,46,74985,38,0,L|392:120,1,56.2500012516975
220,230,75207,2,0,L|209:156,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
282,7,75429,37,0,0:0:0:0:
300,98,75540,1,0,0:0:0:0:
197,25,75651,5,0,0:0:0:0:
222,103,75762,1,0,0:0:0:0:
126,69,75874,5,0,0:0:0:0:
153,134,75985,1,0,0:0:0:0:
76,145,76096,5,0,0:0:0:0:
116,179,76207,1,0,0:0:0:0:
70,222,76318,5,0,0:0:0:0:
111,222,76429,1,0,0:0:0:0:
134,253,76540,6,0,P|135:298|126:314,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
21,384,76762,2,0,P|124:354|260:391,1,224.999996423721,0|0,0:0|0:0,0:0:0:0:
384,366,77207,22,0,L|394:268,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
499,62,77429,2,0,L|486:135,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
507,237,77651,2,0,P|450:231|388:184,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
404,203,77874,2,0,L|313:217,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
113,212,78096,6,0,P|128:267|111:328,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
115,319,78318,2,0,L|213:340,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
274,371,78540,38,0,L|257:186,1,179.999991645813,8|0,3:0|0:0,0:0:0:0:
128,139,78874,1,0,0:0:0:0:
128,139,78985,6,0,L|230:128,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
365,34,79207,37,4,0:0:0:0:
430,114,79318,1,0,0:0:0:0:
361,184,79429,2,0,P|304:170|277:110,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
278,126,79651,2,0,L|189:133,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
64,263,79874,6,0,B|37:230|37:230|50:143,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
66,119,80096,2,0,L|80:210,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
71,361,80318,38,0,B|135:350|135:350|182:305|182:305|243:297,1,179.999991645813,8|0,3:0|0:0,0:0:0:0:
302,247,80651,1,0,0:0:0:0:
222,211,80762,1,0,3:0:0:0:
478,344,80985,5,4,0:0:0:0:
491,309,81096,5,0,0:0:0:0:
498,265,81207,5,8,3:0:0:0:
485,223,81318,5,0,0:0:0:0:
458,179,81429,5,4,0:0:0:0:
418,147,81540,5,0,0:0:0:0:
352,126,81651,5,0,3:0:0:0:
281,149,81762,5,0,0:0:0:0:
239,221,81874,5,4,0:0:0:0:
159,262,81985,5,0,0:0:0:0:
66,234,82096,5,8,3:0:0:0:
11,145,82207,5,0,0:0:0:0:
55,33,82318,5,4,0:0:0:0:
273,44,82540,37,0,3:0:0:0:
320,103,82651,1,0,0:0:0:0:
394,118,82762,1,4,0:0:0:0:
468,100,82874,1,0,0:0:0:0:
507,36,82985,1,8,3:0:0:0:
495,19,83207,5,4,0:0:0:0:
335,83,83318,1,0,0:0:0:0:
453,81,83429,1,0,3:0:0:0:
283,24,83540,2,0,P|196:37|141:120,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
60,238,83874,1,8,3:0:0:0:
21,164,83985,2,0,P|59:149|175:193,1,149.999997615814,0|8,0:0|3:0,0:0:0:0:
252,206,84318,38,0,P|271:160|264:125,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
139,257,84540,2,0,P|131:302|149:340,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
240,379,84762,2,0,B|330:360|330:360|298:344,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
312,351,84985,2,0,P|279:321|270:287,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
359,165,85207,6,0,B|389:202|389:202|368:282,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
373,265,85429,2,0,L|454:282,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
498,139,85651,38,0,P|446:120|396:0,1,179.999991645813,8|0,3:0|0:0,0:0:0:0:
394,13,85985,1,0,0:0:0:0:
301,92,86096,6,0,L|214:83,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
66,66,86318,1,4,0:0:0:0:
13,136,86429,1,0,0:0:0:0:
72,193,86540,2,0,P|120:210|190:178,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
176,192,86762,2,0,P|154:237|160:288,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
309,370,86985,37,0,3:0:0:0:
359,310,87096,1,0,0:0:0:0:
283,297,87207,2,0,L|203:318,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
4,203,87429,2,0,B|55:211|55:211|82:255|82:255|134:266,1,149.999997615814,8|0,3:0|0:0,0:0:0:0:
238,217,87762,1,0,0:0:0:0:
183,120,87874,6,0,L|89:111,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
98,33,88096,2,0,L|23:26,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
306,182,88318,38,0,L|400:173,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
391,95,88540,2,0,L|465:88,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
232,28,88762,2,0,L|220:92,1,37.4999994039535,0|0,3:0|0:0,0:0:0:0:
243,39,88874,2,0,L|231:103,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
256,50,88985,2,0,L|251:87,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
485,87,89207,6,0,L|493:51,3,37.4999994039535,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
396,120,89429,2,0,L|411:197,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
471,317,89651,38,0,P|411:299|320:336,1,149.999997615814,0|4,3:0|0:0,0:0:0:0:
61,239,90096,2,0,P|121:221|212:258,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
367,21,90540,6,0,P|336:57|328:104,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
163,96,90762,2,0,P|194:132|202:179,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
190,346,90985,37,8,3:0:0:0:
328,272,91096,1,0,0:0:0:0:
154,272,91207,5,8,3:0:0:0:
365,338,91318,1,0,0:0:0:0:
257,382,91429,38,0,B|290:333|224:286|269:219,1,149.999997615814,4|4,3:0|0:0,0:0:0:0:
325,196,91762,1,0,0:0:0:0:
325,196,91874,2,0,P|365:210|436:184,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
430,190,92096,2,0,B|418:110,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
313,19,92318,2,0,L|190:36,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
201,34,92540,2,0,B|214:117,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
209,252,92762,5,8,3:0:0:0:
156,261,92874,1,0,0:0:0:0:
112,231,92985,1,4,0:0:0:0:
60,222,93096,1,0,0:0:0:0:
13,247,93207,38,0,P|4:288|19:328,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
173,186,93429,1,4,0:0:0:0:
215,120,93540,1,0,0:0:0:0:
162,49,93651,2,0,P|125:39|76:61,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
234,138,93874,2,0,P|273:157|313:148,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
385,39,94096,5,0,3:0:0:0:
337,286,94318,2,0,L|322:373,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
409,327,94540,2,0,P|418:277|280:230,1,224.999996423721,8|0,3:0|0:0,0:0:0:0:
239,319,94985,2,0,P|218:357|173:373,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
34,344,95207,37,4,0:0:0:0:
21,309,95318,5,0,0:0:0:0:
14,265,95429,5,8,3:0:0:0:
27,223,95540,5,0,0:0:0:0:
54,179,95651,5,4,0:0:0:0:
94,147,95762,5,0,0:0:0:0:
160,126,95873,5,0,3:0:0:0:
231,149,95984,5,0,0:0:0:0:
273,221,96096,5,4,0:0:0:0:
353,262,96207,5,0,0:0:0:0:
446,234,96318,5,8,3:0:0:0:
501,145,96429,5,0,0:0:0:0:
450,36,96540,5,4,0:0:0:0:
239,44,96762,5,0,3:0:0:0:
192,103,96873,1,0,0:0:0:0:
118,118,96984,1,4,0:0:0:0:
44,100,97096,1,0,0:0:0:0:
5,36,97207,1,8,3:0:0:0:
17,19,97429,37,4,0:0:0:0:
146,51,97540,1,0,0:0:0:0:
29,122,97651,2,0,L|39:193,1,56.2499991059302,0|0,3:0|0:0,0:0:0:0:
44,197,97874,6,0,P|100:231|176:201,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
301,160,98096,38,0,P|329:140|382:137,1,84.3750018775463,8|0,3:0|0:0,0:0:0:0:
398,147,98318,6,0,B|431:187|431:187|415:279,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
265,371,98540,38,0,L|180:361,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
127,202,98762,2,0,L|141:113,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
193,260,98985,2,0,P|144:291|68:278,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
91,290,99207,2,0,L|79:373,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
20,184,99429,6,0,B|4:141|4:141|27:66,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
23,78,99651,2,0,L|109:91,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
271,74,99874,2,0,P|254:31|222:12,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
186,180,100096,2,0,P|232:175|260:147,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
132,63,100318,37,0,3:0:0:0:
253,157,100540,1,4,0:0:0:0:
285,167,100651,1,0,0:0:0:0:
357,129,100762,5,8,3:0:0:0:
389,139,100873,1,0,0:0:0:0:
422,148,100985,2,0,P|407:200|416:233,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
459,377,101207,38,0,P|472:333|459:295,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
398,242,101429,2,0,L|314:257,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
165,354,101651,2,0,P|116:332|211:264,1,224.999996423721,8|0,3:0|0:0,0:0:0:0:
302,165,102096,6,0,L|292:89,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
392,91,102318,2,0,L|382:14,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
192,229,102540,38,0,L|212:136,1,74.999998807907,8|0,3:0|0:0,0:0:0:0:
107,172,102762,2,0,L|127:79,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
314,332,102985,6,0,L|305:278,1,37.4999994039535,0|0,3:0|0:0,0:0:0:0:
343,345,103096,2,0,L|334:291,1,37.4999994039535,0|0,0:0|0:0,0:0:0:0:
370,358,103207,2,0,L|361:304,1,37.4999994039535,4|0,0:0|0:0,0:0:0:0:
380,117,103429,38,0,L|374:75,3,37.4999994039535,8|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
444,166,103651,2,0,P|417:188|346:191,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
392,2,103874,2,0,P|424:14|462:74,1,74.999998807907,4|0,3:0|0:0,0:0:0:0:
271,129,104096,2,0,P|265:94|298:31,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
505,113,104318,5,8,3:0:0:0:
269,217,104540,38,0,L|216:216,3,37.4999994039535,0|0|0|0,3:0|3:0|3:0|3:0,0:0:0:0:
360,220,104762,1,0,3:0:0:0:
296,384,104874,1,4,3:0:0:0:
102,307,105096,5,0,0:0:0:0:
102,307,105207,2,0,B|206:381|258:244|374:330,1,269.999987468719,12|0,3:0|0:0,0:0:0:0:
439,319,105651,6,0,P|379:336|396:236,1,168.750003755093,0|0,3:0|0:0,0:0:0:0:
373,258,106096,6,0,P|374:315|443:283,1,112.500002503395,8|0,3:0|0:0,0:0:0:0:
420,323,106651,37,0,0:0:0:0:
469,245,106763,1,4,0:0:0:0:
508,322,106874,1,0,0:0:0:0:
379,245,106985,1,8,3:0:0:0:
483,105,107207,6,0,L|474:40,3,56.2500012516975,4|0|0|0,3:0|0:0|0:0|0:0,0:0:0:0:
462,30,107429,38,0,P|401:56|319:25,1,149.999997615814,0|0,3:0|0:0,0:0:0:0:
272,120,107874,2,0,P|184:91|118:125,1,149.999997615814,8|4,3:0|0:0,0:0:0:0:
103,213,108207,2,0,B|128:232|128:232|269:200,1,149.999997615814,0|0,0:0|0:0,0:0:0:0:
393,187,108540,2,0,L|385:286,1,74.999998807907,4|0,0:0|0:0,0:0:0:0:
333,338,108763,1,8,3:0:0:0:
467,307,108874,6,0,L|509:297,2,37.4999994039535,0|0|4,0:0|0:0|0:0,0:0:0:0:
409,380,109096,1,0,0:0:0:0:
300,257,109207,38,0,P|279:218|281:171,1,74.999998807907,0|0,3:0|0:0,0:0:0:0:
401,118,109429,1,4,0:0:0:0:
401,118,109651,6,0,L|315:109,1,74.999998807907,8|4,3:0|0:0,0:0:0:0:
256,15,109985,37,0,0:0:0:0:
175,121,110096,2,0,P|162:60|109:16,1,112.49999821186,0|0,3:0|0:0,0:0:0:0:
128,26,110318,2,0,P|106:86|47:122,1,112.49999821186,0|0,0:0|0:0,0:0:0:0:
69,114,110540,2,0,P|135:105|185:131,1,112.49999821186,8|0,3:0|0:0,0:0:0:0:
160,223,110762,6,0,B|142:230|142:230|120:228|120:228|95:239|95:239|71:235|71:235|49:244|49:244|22:249,1,112.500002503395,4|0,0:0|0:0,0:0:0:0:
193,334,110985,38,0,P|216:310|242:301,1,56.2500012516975,0|0,3:0|0:0,0:0:0:0:
335,325,111207,2,0,P|366:353|378:379,1,56.2500012516975,0|0,0:0|0:0,0:0:0:0:
273,383,111429,2,0,L|304:213,1,168.750003755093,0|0,0:0|0:0,0:0:0:0:
383,255,111874,22,0,B|422:273|422:273|476:273,1,74.999998807907,8|0,3:0|3:0,0:0:0:0:
209,219,112096,2,0,B|169:221|169:221|131:206,1,74.999998807907,0|0,0:0|0:0,0:0:0:0:
403,147,112318,2,0,B|352:114|352:114|337:43|337:43|295:109|295:109|234:115,1,269.999987468719,8|0,3:0|0:0,0:0:0:0:

Some files were not shown because too many files have changed in this diff Show More