1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 12:25:04 +08:00

Merge branch 'master' into move-difficulty-graph-toggle

This commit is contained in:
Dean Herbert 2022-04-27 17:22:25 +09:00
commit e6bb5e84ec
414 changed files with 8033 additions and 3022 deletions

View File

@ -9,7 +9,7 @@
] ]
}, },
"jetbrains.resharper.globaltools": { "jetbrains.resharper.globaltools": {
"version": "2020.3.2", "version": "2022.1.0-eap10",
"commands": [ "commands": [
"jb" "jb"
] ]
@ -27,7 +27,7 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2022.320.0", "version": "2022.417.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]

View File

@ -9,7 +9,7 @@ body:
Important to note that your issue may have already been reported before. Please check: Important to note that your issue may have already been reported before. Please check:
- Pinned issues, at the top of https://github.com/ppy/osu/issues. - Pinned issues, at the top of https://github.com/ppy/osu/issues.
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0). - Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- And most importantly, search for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful. - And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful.
- type: dropdown - type: dropdown
attributes: attributes:
@ -48,20 +48,28 @@ body:
Attaching log files is required for every reported bug. See instructions below on how to find them. Attaching log files is required for every reported bug. See instructions below on how to find them.
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
### Desktop platforms
If the game has not yet been closed since you found the bug: If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder" 1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there 2. Then open the `logs` folder located there
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead. The default places to find the logs on desktop platforms are as follows:
The default places to find the logs are as follows:
- `%AppData%/osu/logs` *on Windows* - `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux & macOS* - `~/.local/share/osu/logs` *on Linux & macOS*
- `Android/data/sh.ppy.osulazer/files/logs` *on Android*
- *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
If you have selected a custom location for the game files, you can find the `logs` folder there. If you have selected a custom location for the game files, you can find the `logs` folder there.
### Mobile platforms
The places to find the logs on mobile platforms are as follows:
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
---
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below. After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea - type: textarea

View File

@ -2,6 +2,60 @@ on: [push, pull_request]
name: Continuous Integration name: Continuous Integration
jobs: jobs:
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: Restore inspectcode cache
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }}
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
exit_code=0
while read -r line; do
if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- 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
test: test:
name: Test name: Test
runs-on: ${{matrix.os.fullname}} runs-on: ${{matrix.os.fullname}}
@ -93,52 +147,4 @@ jobs:
# cannot accept .sln(f) files as arguments. # cannot accept .sln(f) files as arguments.
# Build just the main game for now. # Build just the main game for now.
- name: Build - name: Build
run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
exit_code=0
while read -r line; do
if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent"> <component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" /> <option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component> </component>

View File

@ -26,14 +26,6 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Project">
<!--
NU1701:
DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway.
This is required due to https://github.com/NuGet/Home/issues/5740
-->
<NoWarn>$(NoWarn);NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup Label="Nuget"> <PropertyGroup Label="Nuget">
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<Authors>ppy Pty Ltd</Authors> <Authors>ppy Pty Ltd</Authors>
@ -42,7 +34,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl> <RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes> <PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company> <Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2021 ppy Pty Ltd</Copyright> <Copyright>Copyright (c) 2022 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags> <PackageTags>osu game</PackageTags>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -5,7 +5,7 @@ dotnet tool restore
# - cmd: dotnet format --dry-run --check # - cmd: dotnet format --dry-run --check
dotnet CodeFileSanity dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
exit $LASTEXITCODE exit $LASTEXITCODE

View File

@ -2,5 +2,5 @@
dotnet tool restore dotnet tool restore
dotnet CodeFileSanity dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors

View File

@ -1,4 +1,4 @@
Copyright (c) 2021 ppy Pty Ltd <contact@ppy.sh>. Copyright (c) 2022 ppy Pty Ltd <contact@ppy.sh>.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>(); protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0]; protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
} }
} }

View File

@ -8,7 +8,7 @@
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl> <PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl> <RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes> <PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<copyright>Copyright (c) 2021 ppy Pty Ltd</copyright> <copyright>Copyright (c) 2022 ppy Pty Ltd</copyright>
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description> <Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
<PackageTags>dotnet-new;templates;osu</PackageTags> <PackageTags>dotnet-new;templates;osu</PackageTags>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.423.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -123,7 +123,12 @@ namespace osu.Desktop
tools.RemoveUninstallerRegistryEntry(); tools.RemoveUninstallerRegistryEntry();
}, onEveryRun: (version, tools, firstRun) => }, onEveryRun: (version, tools, firstRun) =>
{ {
tools.SetProcessAppUserModelId(); // While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
// causes the right-click context menu to function incorrectly.
//
// This may turn out to be non-required after an alternative solution is implemented.
// see https://github.com/clowd/Clowd.Squirrel/issues/24
// tools.SetProcessAppUserModelId();
}); });
} }

View File

@ -19,7 +19,7 @@ namespace osu.Desktop.Security
public class ElevatedPrivilegesChecker : Component public class ElevatedPrivilegesChecker : Component
{ {
[Resolved] [Resolved]
private NotificationOverlay notifications { get; set; } private INotificationOverlay notifications { get; set; }
private bool elevated; private bool elevated;

View File

@ -25,7 +25,7 @@ namespace osu.Desktop.Updater
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{ {
private UpdateManager updateManager; private UpdateManager updateManager;
private NotificationOverlay notificationOverlay; private INotificationOverlay notificationOverlay;
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited(); public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
@ -39,9 +39,9 @@ namespace osu.Desktop.Updater
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger(); private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(NotificationOverlay notification) private void load(INotificationOverlay notifications)
{ {
notificationOverlay = notification; notificationOverlay = notifications;
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger)); SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
} }

View File

@ -24,8 +24,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.8.28-pre" /> <PackageReference Include="Clowd.Squirrel" Version="2.9.23-gc8da1a" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" /> <PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
@ -33,7 +32,6 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.0.175" /> <PackageReference Include="DiscordRichPresence" Version="1.0.175" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">

View File

@ -11,7 +11,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description> <description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes> <releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2021 ppy Pty Ltd</copyright> <copyright>Copyright (c) 2022 ppy Pty Ltd</copyright>
<language>en-AU</language> <language>en-AU</language>
</metadata> </metadata>
<files> <files>

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.0505463516206195d, "diffcalc-test")] [TestCase(4.0505463516206195d, 127, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(5.1696411260785498d, "diffcalc-test")] [TestCase(5.1696411260785498d, 127, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new CatchModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap);

View File

@ -90,6 +90,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new LegacyHitExplosion(); return new LegacyHitExplosion();
return null; return null;
default:
throw new UnsupportedSkinComponentException(component);
} }
} }

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3449735700206298d, "diffcalc-test")] [TestCase(2.3449735700206298d, 151, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(2.7879104989252959d, "diffcalc-test")] [TestCase(2.7879104989252959d, 151, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new ManiaModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap);

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{ {
private const double individual_decay_base = 0.125; private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30; private const double overall_decay_base = 0.30;
private const double release_threshold = 24;
protected override double SkillMultiplier => 1; protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1; protected override double StrainDecayBase => 1;
@ -37,31 +38,43 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
var maniaCurrent = (ManiaDifficultyHitObject)current; var maniaCurrent = (ManiaDifficultyHitObject)current;
double endTime = maniaCurrent.EndTime; double endTime = maniaCurrent.EndTime;
int column = maniaCurrent.BaseObject.Column; int column = maniaCurrent.BaseObject.Column;
double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.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 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 double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
bool isOverlapping = false;
// Fill up the holdEndTimes array // Fill up the holdEndTimes array
for (int i = 0; i < holdEndTimes.Length; ++i) for (int i = 0; i < holdEndTimes.Length; ++i)
{ {
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut... // The current note is overlapped if a previous note or end is overlapping the current note body
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1)) isOverlapping |= Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1);
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
holdAddition = 0;
// We give a slight bonus to everything if something is held meanwhile // We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1)) if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
holdFactor = 1.25; holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i]));
// Decay individual strains // Decay individual strains
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base); individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
} }
holdEndTimes[column] = endTime; holdEndTimes[column] = 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 = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
// Increase individual strain in own column // Increase individual strain in own column
individualStrains[column] += 2.0 * holdFactor; individualStrains[column] += 2.0 * holdFactor;
individualStrain = individualStrains[column]; individualStrain = individualStrains[column];

View File

@ -116,9 +116,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
case ManiaSkinComponents.StageForeground: case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground(); return new LegacyStageForeground();
}
break; default:
throw new UnsupportedSkinComponentException(component);
}
} }
return base.GetDrawableComponent(component); return base.GetDrawableComponent(component);

View File

@ -0,0 +1,108 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Diagnostics;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[HeadlessTest]
public class LegacyMainCirclePieceTest : OsuTestScene
{
private static readonly object?[][] texture_priority_cases =
{
// default priority lookup
new object?[]
{
// available textures
new[] { @"hitcircle", @"hitcircleoverlay" },
// priority lookup prefix
null,
// expected circle and overlay
@"hitcircle", @"hitcircleoverlay",
},
// custom priority lookup
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle", @"sliderstartcircleoverlay" },
@"sliderstartcircle",
@"sliderstartcircle", @"sliderstartcircleoverlay",
},
// when no sprites are available for the specified prefix, fall back to "hitcircle"/"hitcircleoverlay".
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay" },
@"sliderstartcircle",
@"hitcircle", @"hitcircleoverlay",
},
// when a circle is available for the specified prefix but no overlay exists, no overlay is displayed.
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle" },
@"sliderstartcircle",
@"sliderstartcircle", null
},
// when no circle is available for the specified prefix but an overlay exists, the overlay is ignored.
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircleoverlay" },
@"sliderstartcircle",
@"hitcircle", @"hitcircleoverlay",
}
};
[TestCaseSource(nameof(texture_priority_cases))]
public void TestTexturePriorities(string[] textureFilenames, string priorityLookup, string? expectedCircle, string? expectedOverlay)
{
TestLegacyMainCirclePiece piece = null!;
AddStep("load circle piece", () =>
{
var skin = new Mock<ISkinSource>();
// shouldn't be required as GetTexture(string) calls GetTexture(string, WrapMode, WrapMode) by default,
// but moq doesn't handle that well, therefore explicitly requiring to use `CallBase`:
// https://github.com/moq/moq4/issues/972
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
.Returns((string componentName, WrapMode _, WrapMode __) => new Texture(1, 1) { AssetName = componentName });
Child = new DependencyProvidingContainer
{
CachedDependencies = new (Type, object)[] { (typeof(ISkinSource), skin.Object) },
Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
};
var sprites = this.ChildrenOfType<Sprite>().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray();
Debug.Assert(sprites.Length <= 2);
});
AddAssert("check circle sprite", () => piece.CircleSprite?.Texture?.AssetName == expectedCircle);
AddAssert("check overlay sprite", () => piece.OverlaySprite?.Texture?.AssetName == expectedOverlay);
}
private class TestLegacyMainCirclePiece : LegacyMainCirclePiece
{
public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public TestLegacyMainCirclePiece(string? priorityLookupPrefix)
: base(priorityLookupPrefix, false)
{
}
}
}
}

View File

@ -6,18 +6,18 @@ using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
public class TestSceneOsuModAimAssist : OsuModTestScene public class TestSceneOsuModMagnetised : OsuModTestScene
{ {
[TestCase(0.1f)] [TestCase(0.1f)]
[TestCase(0.5f)] [TestCase(0.5f)]
[TestCase(1)] [TestCase(1)]
public void TestAimAssist(float strength) public void TestMagnetised(float strength)
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new OsuModAimAssist Mod = new OsuModMagnetised
{ {
AssistStrength = { Value = strength }, AttractionStrength = { Value = strength },
}, },
PassCondition = () => true, PassCondition = () => true,
Autoplay = false, Autoplay = false,

View File

@ -15,15 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6972307565739273d, "diffcalc-test")] [TestCase(6.6972307565739273d, 206, "diffcalc-test")]
[TestCase(1.4484754139145539d, "zero-length-sliders")] [TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9382559208689809d, "diffcalc-test")] [TestCase(8.9382559208689809d, 206, "diffcalc-test")]
[TestCase(1.7548875851757628d, "zero-length-sliders")] [TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6972307218715166d, 239, "diffcalc-test")]
[TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);

View File

@ -70,7 +70,9 @@ namespace osu.Game.Rulesets.Osu.Tests
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo()); var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1"; tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
Child = new SkinProvidingContainer(tintingSkin) var provider = Ruleset.Value.CreateInstance().CreateLegacySkinProvider(tintingSkin, Beatmap.Value.Beatmap);
Child = new SkinProvidingContainer(provider)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = dho = new DrawableSlider(prepareObject(new Slider Child = dho = new DrawableSlider(prepareObject(new Slider

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps namespace osu.Game.Rulesets.Osu.Beatmaps
@ -20,13 +21,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Circle Count", Name = BeatmapsetsStrings.ShowStatsCountCircles,
Content = circles.ToString(), Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Slider Count", Name = BeatmapsetsStrings.ShowStatsCountSliders,
Content = sliders.ToString(), Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
}, },

View File

@ -61,10 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate; double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.GetMaxCombo();
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int sliderCount = beatmap.HitObjects.Count(h => h is Slider);

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
public bool PerformFail() => false; public bool PerformFail() => false;

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutoplay : ModAutoplay public class OsuModAutoplay : ModAutoplay
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => 1.12;
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
private DrawableOsuBlinds blinds; private DrawableOsuBlinds blinds;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModCinema : ModCinema<OsuHitObject> public class OsuModCinema : ModCinema<OsuHitObject>
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
{ {
public override double ScoreMultiplier => 1.12; public override double ScoreMultiplier => 1.12;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
private const double default_follow_delay = 120; private const double default_follow_delay = 120;

View File

@ -16,20 +16,20 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject> internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{ {
public override string Name => "Aim Assist"; public override string Name => "Magnetised";
public override string Acronym => "AA"; public override string Acronym => "MG";
public override IconUsage? Icon => FontAwesome.Solid.MousePointer; public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circle the circle chases you!"; public override string Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
private IFrameStableClock gameplayClock; private IFrameStableClock gameplayClock;
[SettingSource("Assist strength", "How much this mod will assist you.", 0)] [SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f) public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
{ {
Precision = 0.05f, Precision = 0.05f,
MinValue = 0.05f, MinValue = 0.05f,
@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void easeTo(DrawableHitObject hitObject, Vector2 destination) private void easeTo(DrawableHitObject hitObject, Vector2 destination)
{ {
double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value); double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime); float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1; protected virtual float EndScale => 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) }; public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{ {

View File

@ -4,19 +4,14 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils; using osu.Game.Rulesets.Osu.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
@ -28,12 +23,6 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "It never gets boring!"; public override string Description => "It never gets boring!";
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast; private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
/// <summary>
/// Number of previous hitobjects to be shifted together when another object is being moved.
/// </summary>
private const int preceding_hitobjects_to_shift = 10;
private Random? rng; private Random? rng;
@ -42,330 +31,33 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!(beatmap is OsuBeatmap osuBeatmap)) if (!(beatmap is OsuBeatmap osuBeatmap))
return; return;
var hitObjects = osuBeatmap.HitObjects;
Seed.Value ??= RNG.Next(); Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value); rng = new Random((int)Seed.Value);
var randomObjects = randomiseObjects(hitObjects); var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects);
applyRandomisation(hitObjects, randomObjects);
}
/// <summary>
/// Randomise the position of each hit object and return a list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.
/// </summary>
/// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to have their positions randomised.</param>
/// <returns>A list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.</returns>
private List<RandomObjectInfo> randomiseObjects(IEnumerable<OsuHitObject> hitObjects)
{
Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects");
var randomObjects = new List<RandomObjectInfo>();
RandomObjectInfo? previous = null;
float rateOfChangeMultiplier = 0; float rateOfChangeMultiplier = 0;
foreach (OsuHitObject hitObject in hitObjects) foreach (var positionInfo in positionInfos)
{ {
var current = new RandomObjectInfo(hitObject);
randomObjects.Add(current);
// rateOfChangeMultiplier only changes every 5 iterations in a combo // rateOfChangeMultiplier only changes every 5 iterations in a combo
// to prevent shaky-line-shaped streams // to prevent shaky-line-shaped streams
if (hitObject.IndexInCurrentCombo % 5 == 0) if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1; rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
if (previous == null) if (positionInfo == positionInfos.First())
{ {
current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2); positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI); positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
} }
else else
{ {
current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal); positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f));
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
// is proportional to the distance between the last and the current hit object
// to allow jumps and prevent too sharp turns during streams.
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f));
} }
previous = current;
} }
return randomObjects; osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
}
/// <summary>
/// Reposition the hit objects according to the information in <paramref name="randomObjects"/>.
/// </summary>
/// <param name="hitObjects">The hit objects to be repositioned.</param>
/// <param name="randomObjects">A list of <see cref="RandomObjectInfo"/> describing how each hit object should be placed.</param>
private void applyRandomisation(IReadOnlyList<OsuHitObject> hitObjects, IReadOnlyList<RandomObjectInfo> randomObjects)
{
RandomObjectInfo? previous = null;
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
var current = randomObjects[i];
if (hitObject is Spinner)
{
previous = null;
continue;
}
computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null);
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2.Zero;
switch (hitObject)
{
case HitCircle circle:
shift = clampHitCircleToPlayfield(circle, current);
break;
case Slider slider:
shift = clampSliderToPlayfield(slider, current);
break;
}
if (shift != Vector2.Zero)
{
var toBeShifted = new List<OsuHitObject>();
for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
{
// only shift hit circles
if (!(hitObjects[j] is HitCircle)) break;
toBeShifted.Add(hitObjects[j]);
}
if (toBeShifted.Count > 0)
applyDecreasingShift(toBeShifted, shift);
}
previous = current;
}
}
/// <summary>
/// Compute the randomised position of a hit object while attempting to keep it inside the playfield.
/// </summary>
/// <param name="current">The <see cref="RandomObjectInfo"/> representing the hit object to have the randomised position computed for.</param>
/// <param name="previous">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the current one.</param>
/// <param name="beforePrevious">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param>
private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious)
{
float previousAbsoluteAngle = 0f;
if (previous != null)
{
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
}
float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle;
var posRelativeToPrev = new Vector2(
current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
);
Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre;
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
current.PositionRandomised = lastEndPosition + posRelativeToPrev;
}
/// <summary>
/// Move the randomised position of a hit circle so that it fits inside the playfield.
/// </summary>
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo)
{
var previousPosition = objectInfo.PositionRandomised;
objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding(
objectInfo.PositionRandomised,
(float)circle.Radius
);
circle.Position = objectInfo.PositionRandomised;
return objectInfo.PositionRandomised - previousPosition;
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo)
{
var possibleMovementBounds = calculatePossibleMovementBounds(slider);
var previousPosition = objectInfo.PositionRandomised;
// Clamp slider position to the placement area
// If the slider is larger than the playfield, force it to stay at the original position
float newX = possibleMovementBounds.Width < 0
? objectInfo.PositionOriginal.X
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
float newY = possibleMovementBounds.Height < 0
? objectInfo.PositionOriginal.Y
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY);
objectInfo.EndPositionRandomised = slider.EndPosition;
shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal);
return objectInfo.PositionRandomised - previousPosition;
}
/// <summary>
/// Decreasingly shift a list of <see cref="OsuHitObject"/>s by a specified amount.
/// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
/// </summary>
/// <param name="hitObjects">The list of hit objects to be shifted.</param>
/// <param name="shift">The amount to be shifted.</param>
private void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2 shift)
{
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
// The first object is shifted by a vector slightly smaller than shift
// The last object is shifted by a vector slightly larger than zero
Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
}
}
/// <summary>
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
private RectangleF calculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float minY = float.PositiveInfinity;
float maxY = float.NegativeInfinity;
// Compute the bounding box of the slider.
foreach (var pos in pathPositions)
{
minX = MathF.Min(minX, pos.X);
maxX = MathF.Max(maxX, pos.X);
minY = MathF.Min(minY, pos.Y);
maxY = MathF.Max(maxY, pos.Y);
}
// Take the circle radius into account.
float radius = (float)slider.Radius;
minX -= radius;
minY -= radius;
maxX += radius;
maxY += radius;
// Given the bounding box of the slider (via min/max X/Y),
// the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
// and the amount that it can move to the right is WIDTH - maxX.
// Same calculation applies for the Y axis.
float left = -minX;
float right = OsuPlayfield.BASE_SIZE.X - maxX;
float top = -minY;
float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
return new RectangleF(left, top, right - left, bottom - top);
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private void shiftNestedObjects(Slider slider, Vector2 shift)
{
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
osuHitObject.Position += shift;
}
}
/// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary>
/// <param name="position">The position to be clamped.</param>
/// <param name="padding">The minimum distance allowed from playfield edges.</param>
/// <returns>The clamped position.</returns>
private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
{
return new Vector2(
Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
);
}
private class RandomObjectInfo
{
/// <summary>
/// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
/// </summary>
/// <remarks>
/// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
/// </remarks>
/// <example>
/// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing
/// the previous object to reach this one.
/// </example>
public float RelativeAngle { get; set; }
/// <summary>
/// The jump distance from the previous hit object to this one.
/// </summary>
/// <remarks>
/// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center.
/// </remarks>
public float DistanceFromPrevious { get; set; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionRandomised { get; set; }
public Vector2 EndPositionOriginal { get; }
public Vector2 EndPositionRandomised { get; set; }
public OsuHitObject HitObject { get; }
public RandomObjectInfo(OsuHitObject hitObject)
{
PositionRandomised = PositionOriginal = hitObject.Position;
EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
HitObject = hitObject;
}
} }
} }
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{ {
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModAimAssist) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray();
/// <summary> /// <summary>
/// How early before a hitobject's start time to trigger a hit. /// How early before a hitobject's start time to trigger a hit.

View File

@ -9,6 +9,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModSuddenDeath : ModSuddenDeath public class OsuModSuddenDeath : ModSuddenDeath
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
{
typeof(OsuModAutopilot),
typeof(OsuModTarget),
}).ToArray();
} }
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"Practice keeping up with the beat of the song."; public override string Description => @"Practice keeping up with the beat of the song.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles) }; public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSuddenDeath) };
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> Seed { get; } = new Bindable<int?> public Bindable<int?> Seed { get; } = new Bindable<int?>

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING."; public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) };
private float theta; private float theta;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still..."; public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles private const int wiggle_strength = 10; // Higher = stronger wiggles

View File

@ -6,10 +6,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Osu.UI;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -21,10 +21,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public readonly IBindable<float> ScaleBindable = new BindableFloat(); public readonly IBindable<float> ScaleBindable = new BindableFloat();
public readonly IBindable<int> IndexInCurrentComboBindable = new Bindable<int>(); public readonly IBindable<int> IndexInCurrentComboBindable = new Bindable<int>();
// Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects. // Must be set to update IsHovered as it's used in relax mod to detect osu hit objects.
public override bool HandlePositionalInput => true; public override bool HandlePositionalInput => true;
protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X; protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
/// <summary> /// <summary>
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value. /// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
@ -89,6 +89,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary> /// </summary>
public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent.ScreenSpaceDrawQuad.AABBFloat;
/// <summary>
/// Calculates the position of the given <paramref name="drawable"/> relative to the playfield area.
/// </summary>
/// <param name="drawable">The drawable to calculate its relative position.</param>
protected float CalculateDrawableRelativePosition(Drawable drawable) => (drawable.ScreenSpaceDrawQuad.Centre.X - parentScreenSpaceRectangle.X) / parentScreenSpaceRectangle.Width;
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
} }
} }

View File

@ -13,7 +13,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -208,7 +207,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Tracking.Value && slidingSample != null) if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position // keep the sliding sample playing at the current tracking position
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X); slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1); double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);

View File

@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(), new OsuModApproachDifferent(),
new OsuModMuted(), new OsuModMuted(),
new OsuModNoScope(), new OsuModNoScope(),
new OsuModAimAssist(), new OsuModMagnetised(),
new ModAdaptiveSpeed() new ModAdaptiveSpeed()
}; };

View File

@ -3,10 +3,10 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -16,63 +16,61 @@ using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
#nullable enable
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class LegacyMainCirclePiece : CompositeDrawable public class LegacyMainCirclePiece : CompositeDrawable
{ {
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
private readonly string priorityLookup; /// <summary>
/// A prioritised prefix to perform texture lookups with.
/// </summary>
private readonly string? priorityLookupPrefix;
private readonly bool hasNumber; private readonly bool hasNumber;
public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true) protected Drawable CircleSprite = null!;
protected Drawable OverlaySprite = null!;
protected Container OverlayLayer { get; private set; } = null!;
private SkinnableSpriteText hitCircleText = null!;
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[Resolved(canBeNull: true)]
private DrawableHitObject? drawableObject { get; set; }
[Resolved]
private ISkinSource skin { get; set; } = null!;
public LegacyMainCirclePiece(string? priorityLookupPrefix = null, bool hasNumber = true)
{ {
this.priorityLookup = priorityLookup; this.priorityLookupPrefix = priorityLookupPrefix;
this.hasNumber = hasNumber; this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
} }
private Drawable hitCircleSprite;
protected Container OverlayLayer { get; private set; }
private Drawable hitCircleOverlay;
private SkinnableSpriteText hitCircleText;
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[Resolved]
private DrawableHitObject drawableObject { get; set; }
[Resolved]
private ISkinSource skin { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var drawableOsuObject = (DrawableOsuHitObject)drawableObject; var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
bool allowFallback = false; // if a base texture for the specified prefix exists, continue using it for subsequent lookups.
// otherwise fall back to the default prefix "hitcircle".
// attempt lookup using priority specification string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle";
Texture baseTexture = getTextureWithFallback(string.Empty);
// if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
if (baseTexture == null)
{
allowFallback = true;
baseTexture = getTextureWithFallback(string.Empty);
}
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it. // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist. // the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin). // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
InternalChildren = new[] InternalChildren = new[]
{ {
hitCircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = baseTexture }) CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -81,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Child = hitCircleOverlay = new KiaiFlashingDrawable(() => getAnimationWithFallback(@"overlay", 1000 / 2d)) Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -105,39 +103,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
if (overlayAboveNumber) if (overlayAboveNumber)
OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue); OverlayLayer.ChangeChildDepth(OverlaySprite, float.MinValue);
accentColour.BindTo(drawableObject.AccentColour); if (drawableOsuObject != null)
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
Texture getTextureWithFallback(string name)
{ {
Texture tex = null; accentColour.BindTo(drawableOsuObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
if (!string.IsNullOrEmpty(priorityLookup))
{
tex = skin.GetTexture($"{priorityLookup}{name}");
if (!allowFallback)
return tex;
}
return tex ?? skin.GetTexture($"hitcircle{name}");
}
Drawable getAnimationWithFallback(string name, double frameLength)
{
Drawable animation = null;
if (!string.IsNullOrEmpty(priorityLookup))
{
animation = skin.GetAnimation($"{priorityLookup}{name}", true, true, frameLength: frameLength);
if (!allowFallback)
return animation;
}
return animation ?? skin.GetAnimation($"hitcircle{name}", true, true, frameLength: frameLength);
} }
} }
@ -145,28 +116,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
base.LoadComplete(); base.LoadComplete();
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); accentColour.BindValueChanged(colour => CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber) if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms; if (drawableObject != null)
updateStateTransforms(drawableObject, drawableObject.State.Value); {
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
}
} }
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{ {
const double legacy_fade_duration = 240; const double legacy_fade_duration = 240;
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) using (BeginAbsoluteSequence(drawableObject.AsNonNull().HitStateUpdateTime))
{ {
switch (state) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out); CircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out); OverlaySprite.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber) if (hasNumber)
{ {

View File

@ -35,6 +35,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinComponents.FollowPoint: case OsuSkinComponents.FollowPoint:
return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false); return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false);
case OsuSkinComponents.SliderScorePoint:
return this.GetAnimation(component.LookupName, false, false);
case OsuSkinComponents.SliderFollowCircle: case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true); var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
if (followCircle != null) if (followCircle != null)
@ -123,6 +126,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinComponents.ApproachCircle: case OsuSkinComponents.ApproachCircle:
return new LegacyApproachCircle(); return new LegacyApproachCircle();
default:
throw new UnsupportedSkinComponentException(component);
} }
} }

View File

@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Utils namespace osu.Game.Rulesets.Osu.Utils
{ {
public static class OsuHitObjectGenerationUtils public static partial class OsuHitObjectGenerationUtils
{ {
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle. // The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn // The closer the hit objects draw to the border, the sharper the turn

View File

@ -0,0 +1,340 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
#nullable enable
namespace osu.Game.Rulesets.Osu.Utils
{
public static partial class OsuHitObjectGenerationUtils
{
/// <summary>
/// Number of previous hitobjects to be shifted together when an object is being moved.
/// </summary>
private const int preceding_hitobjects_to_shift = 10;
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
/// <summary>
/// Generate a list of <see cref="ObjectPositionInfo"/>s containing information for how the given list of
/// <see cref="OsuHitObject"/>s are positioned.
/// </summary>
/// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to process.</param>
/// <returns>A list of <see cref="ObjectPositionInfo"/>s describing how each hit object is positioned relative to the previous one.</returns>
public static List<ObjectPositionInfo> GeneratePositionInfos(IEnumerable<OsuHitObject> hitObjects)
{
var positionInfos = new List<ObjectPositionInfo>();
Vector2 previousPosition = playfield_centre;
float previousAngle = 0;
foreach (OsuHitObject hitObject in hitObjects)
{
Vector2 relativePosition = hitObject.Position - previousPosition;
float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
float relativeAngle = absoluteAngle - previousAngle;
positionInfos.Add(new ObjectPositionInfo(hitObject)
{
RelativeAngle = relativeAngle,
DistanceFromPrevious = relativePosition.Length
});
previousPosition = hitObject.EndPosition;
previousAngle = absoluteAngle;
}
return positionInfos;
}
/// <summary>
/// Reposition the hit objects according to the information in <paramref name="objectPositionInfos"/>.
/// </summary>
/// <param name="objectPositionInfos">Position information for each hit object.</param>
/// <returns>The repositioned hit objects.</returns>
public static List<OsuHitObject> RepositionHitObjects(IEnumerable<ObjectPositionInfo> objectPositionInfos)
{
List<WorkingObject> workingObjects = objectPositionInfos.Select(o => new WorkingObject(o)).ToList();
WorkingObject? previous = null;
for (int i = 0; i < workingObjects.Count; i++)
{
var current = workingObjects[i];
var hitObject = current.HitObject;
if (hitObject is Spinner)
{
previous = null;
continue;
}
computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null);
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2.Zero;
switch (hitObject)
{
case HitCircle _:
shift = clampHitCircleToPlayfield(current);
break;
case Slider _:
shift = clampSliderToPlayfield(current);
break;
}
if (shift != Vector2.Zero)
{
var toBeShifted = new List<OsuHitObject>();
for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
{
// only shift hit circles
if (!(workingObjects[j].HitObject is HitCircle)) break;
toBeShifted.Add(workingObjects[j].HitObject);
}
if (toBeShifted.Count > 0)
applyDecreasingShift(toBeShifted, shift);
}
previous = current;
}
return workingObjects.Select(p => p.HitObject).ToList();
}
/// <summary>
/// Compute the modified position of a hit object while attempting to keep it inside the playfield.
/// </summary>
/// <param name="current">The <see cref="WorkingObject"/> representing the hit object to have the modified position computed for.</param>
/// <param name="previous">The <see cref="WorkingObject"/> representing the hit object immediately preceding the current one.</param>
/// <param name="beforePrevious">The <see cref="WorkingObject"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param>
private static void computeModifiedPosition(WorkingObject current, WorkingObject? previous, WorkingObject? beforePrevious)
{
float previousAbsoluteAngle = 0f;
if (previous != null)
{
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
}
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
var posRelativeToPrev = new Vector2(
current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
);
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
current.PositionModified = lastEndPosition + posRelativeToPrev;
}
/// <summary>
/// Move the modified position of a <see cref="HitCircle"/> so that it fits inside the playfield.
/// </summary>
/// <returns>The deviation from the original modified position in order to fit within the playfield.</returns>
private static Vector2 clampHitCircleToPlayfield(WorkingObject workingObject)
{
var previousPosition = workingObject.PositionModified;
workingObject.EndPositionModified = workingObject.PositionModified = clampToPlayfieldWithPadding(
workingObject.PositionModified,
(float)workingObject.HitObject.Radius
);
workingObject.HitObject.Position = workingObject.PositionModified;
return workingObject.PositionModified - previousPosition;
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
/// <returns>The deviation from the original modified position in order to fit within the playfield.</returns>
private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
{
var slider = (Slider)workingObject.HitObject;
var possibleMovementBounds = calculatePossibleMovementBounds(slider);
var previousPosition = workingObject.PositionModified;
// Clamp slider position to the placement area
// If the slider is larger than the playfield, force it to stay at the original position
float newX = possibleMovementBounds.Width < 0
? workingObject.PositionOriginal.X
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
float newY = possibleMovementBounds.Height < 0
? workingObject.PositionOriginal.Y
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
workingObject.EndPositionModified = slider.EndPosition;
shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
return workingObject.PositionModified - previousPosition;
}
/// <summary>
/// Decreasingly shift a list of <see cref="OsuHitObject"/>s by a specified amount.
/// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
/// </summary>
/// <param name="hitObjects">The list of hit objects to be shifted.</param>
/// <param name="shift">The amount to be shifted.</param>
private static void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2 shift)
{
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
// The first object is shifted by a vector slightly smaller than shift
// The last object is shifted by a vector slightly larger than zero
Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
}
}
/// <summary>
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
private static RectangleF calculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float minY = float.PositiveInfinity;
float maxY = float.NegativeInfinity;
// Compute the bounding box of the slider.
foreach (var pos in pathPositions)
{
minX = MathF.Min(minX, pos.X);
maxX = MathF.Max(maxX, pos.X);
minY = MathF.Min(minY, pos.Y);
maxY = MathF.Max(maxY, pos.Y);
}
// Take the circle radius into account.
float radius = (float)slider.Radius;
minX -= radius;
minY -= radius;
maxX += radius;
maxY += radius;
// Given the bounding box of the slider (via min/max X/Y),
// the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
// and the amount that it can move to the right is WIDTH - maxX.
// Same calculation applies for the Y axis.
float left = -minX;
float right = OsuPlayfield.BASE_SIZE.X - maxX;
float top = -minY;
float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
return new RectangleF(left, top, right - left, bottom - top);
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private static void shiftNestedObjects(Slider slider, Vector2 shift)
{
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
osuHitObject.Position += shift;
}
}
/// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary>
/// <param name="position">The position to be clamped.</param>
/// <param name="padding">The minimum distance allowed from playfield edges.</param>
/// <returns>The clamped position.</returns>
private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
{
return new Vector2(
Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
);
}
public class ObjectPositionInfo
{
/// <summary>
/// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
/// </summary>
/// <remarks>
/// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
/// </remarks>
/// <example>
/// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing
/// the previous object to reach this one.
/// </example>
public float RelativeAngle { get; set; }
/// <summary>
/// The jump distance from the previous hit object to this one.
/// </summary>
/// <remarks>
/// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center.
/// </remarks>
public float DistanceFromPrevious { get; set; }
/// <summary>
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
/// </summary>
public OsuHitObject HitObject { get; }
public ObjectPositionInfo(OsuHitObject hitObject)
{
HitObject = hitObject;
}
}
private class WorkingObject
{
public Vector2 PositionOriginal { get; }
public Vector2 PositionModified { get; set; }
public Vector2 EndPositionModified { get; set; }
public ObjectPositionInfo PositionInfo { get; }
public OsuHitObject HitObject => PositionInfo.HitObject;
public WorkingObject(ObjectPositionInfo positionInfo)
{
PositionInfo = positionInfo;
PositionModified = PositionOriginal = HitObject.Position;
EndPositionModified = HitObject.EndPosition;
}
}
}
}

View File

@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[TestCase(2.2420075288523802d, "diffcalc-test")] [TestCase(2.2420075288523802d, 200, "diffcalc-test")]
[TestCase(2.2420075288523802d, "diffcalc-test-strong")] [TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(3.134084469440479d, "diffcalc-test")] [TestCase(3.134084469440479d, 200, "diffcalc-test")]
[TestCase(3.134084469440479d, "diffcalc-test-strong")] [TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new TaikoModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap);

View File

@ -28,9 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
// flying hits all land in one common scrolling container (and stay there for rewind purposes), // flying hits all land in one common scrolling container (and stay there for rewind purposes),
// so we need to manually get the latest one. // so we need to manually get the latest one.
flyingHit = this.ChildrenOfType<DrawableFlyingHit>() flyingHit = this.ChildrenOfType<DrawableFlyingHit>().MaxBy(h => h.HitObject.StartTime);
.OrderByDescending(h => h.HitObject.StartTime)
.FirstOrDefault();
}); });
AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType); AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType);

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// <summary> /// <summary>
/// Default size of a drawable taiko hit object. /// Default size of a drawable taiko hit object.
/// </summary> /// </summary>
public const float DEFAULT_SIZE = 0.45f; public const float DEFAULT_SIZE = 0.475f;
public override Judgement CreateJudgement() => new TaikoJudgement(); public override Judgement CreateJudgement() => new TaikoJudgement();

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// <summary> /// <summary>
/// Scale multiplier for a strong drawable taiko hit object. /// Scale multiplier for a strong drawable taiko hit object.
/// </summary> /// </summary>
public const float STRONG_SCALE = 1.4f; public const float STRONG_SCALE = 1 / 0.65f;
/// <summary> /// <summary>
/// Default size of a strong drawable taiko hit object. /// Default size of a strong drawable taiko hit object.

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Default namespace osu.Game.Rulesets.Taiko.Skinning.Default
@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
/// </summary> /// </summary>
public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
{ {
public const float SYMBOL_SIZE = 0.45f; public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_SIZE;
public const float SYMBOL_BORDER = 8; public const float SYMBOL_BORDER = 8;
private const double pre_beat_transition_time = 80; private const double pre_beat_transition_time = 80;
private Color4 accentColour; private Color4 accentColour;

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite new Sprite
{ {
Texture = skin.GetTexture("approachcircle"), Texture = skin.GetTexture("approachcircle"),
Scale = new Vector2(0.73f), Scale = new Vector2(0.83f),
Alpha = 0.47f, // eyeballed to match stable Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite new Sprite
{ {
Texture = skin.GetTexture("taikobigcircle"), Texture = skin.GetTexture("taikobigcircle"),
Scale = new Vector2(0.7f), Scale = new Vector2(0.8f),
Alpha = 0.22f, // eyeballed to match stable Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -57,6 +57,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.DrumRollTick: case TaikoSkinComponents.DrumRollTick:
return this.GetAnimation("sliderscorepoint", false, false); return this.GetAnimation("sliderscorepoint", false, false);
case TaikoSkinComponents.Swell:
// todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601).
return null;
case TaikoSkinComponents.HitTarget: case TaikoSkinComponents.HitTarget:
if (GetTexture("taikobigcircle") != null) if (GetTexture("taikobigcircle") != null)
return new TaikoLegacyHitTarget(); return new TaikoLegacyHitTarget();
@ -119,6 +123,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.Mascot: case TaikoSkinComponents.Mascot:
return new DrawableTaikoMascot(); return new DrawableTaikoMascot();
default:
throw new UnsupportedSkinComponentException(component);
} }
} }

View File

@ -160,6 +160,40 @@ namespace osu.Game.Tests.Gameplay
assertHealthNotEqualTo(1); assertHealthNotEqualTo(1);
} }
[Test]
public void TestFailConditions()
{
var beatmap = createBeatmap(0, 1000);
createProcessor(beatmap);
AddStep("setup fail conditions", () => processor.FailConditions += ((_, result) => result.Type == HitResult.Miss));
AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
AddAssert("not failed", () => !processor.HasFailed);
AddStep("apply miss hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Miss }));
AddAssert("failed", () => processor.HasFailed);
}
[TestCase(HitResult.Miss)]
[TestCase(HitResult.Meh)]
public void TestMultipleFailConditions(HitResult resultApplied)
{
var beatmap = createBeatmap(0, 1000);
createProcessor(beatmap);
AddStep("setup multiple fail conditions", () =>
{
processor.FailConditions += ((_, result) => result.Type == HitResult.Miss);
processor.FailConditions += ((_, result) => result.Type == HitResult.Meh);
});
AddStep("apply perfect hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect }));
AddAssert("not failed", () => !processor.HasFailed);
AddStep($"apply {resultApplied.ToString().ToLower()} hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = resultApplied }));
AddAssert("failed", () => processor.HasFailed);
}
[Test] [Test]
public void TestBonusObjectsExcludedFromDrain() public void TestBonusObjectsExcludedFromDrain()
{ {

View File

@ -26,6 +26,12 @@ namespace osu.Game.Tests.Gameplay
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
} }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("reset audio offset", () => localConfig.SetValue(OsuSetting.AudioOffset, 0.0));
}
[Test] [Test]
public void TestStartThenElapsedTime() public void TestStartThenElapsedTime()
{ {
@ -36,7 +42,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
}); });
AddStep("start clock", () => gameplayClockContainer.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
@ -53,7 +59,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
}); });
AddStep("start clock", () => gameplayClockContainer.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
@ -73,26 +79,29 @@ namespace osu.Game.Tests.Gameplay
public void TestSeekPerformsInGameplayTime( public void TestSeekPerformsInGameplayTime(
[Values(1.0, 0.5, 2.0)] double clockRate, [Values(1.0, 0.5, 2.0)] double clockRate,
[Values(0.0, 200.0, -200.0)] double userOffset, [Values(0.0, 200.0, -200.0)] double userOffset,
[Values(false, true)] bool whileStopped) [Values(false, true)] bool whileStopped,
[Values(false, true)] bool setAudioOffsetBeforeConstruction)
{ {
ClockBackedTestWorkingBeatmap working = null; ClockBackedTestWorkingBeatmap working = null;
GameplayClockContainer gameplayClockContainer = null; GameplayClockContainer gameplayClockContainer = null;
if (setAudioOffsetBeforeConstruction)
AddStep($"preset audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("create container", () => AddStep("create container", () =>
{ {
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio); working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
working.LoadTrack(); working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
if (whileStopped) gameplayClockContainer.Reset(startClock: !whileStopped);
gameplayClockContainer.Stop();
gameplayClockContainer.Reset();
}); });
AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate))); AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate)));
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
if (!setAudioOffsetBeforeConstruction)
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500)); AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f)); AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));

View File

@ -88,7 +88,7 @@ namespace osu.Game.Tests.Gameplay
[Test] [Test]
public void TestSampleHasLifetimeEndWithInitialClockTime() public void TestSampleHasLifetimeEndWithInitialClockTime()
{ {
GameplayClockContainer gameplayContainer = null; MasterGameplayClockContainer gameplayContainer = null;
DrawableStoryboardSample sample = null; DrawableStoryboardSample sample = null;
AddStep("create container", () => AddStep("create container", () =>
@ -96,8 +96,11 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true) const double start_time = 1000;
Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time)
{ {
StartTime = start_time,
IsPaused = { Value = true }, IsPaused = { Value = true },
Child = new FrameStabilityContainer Child = new FrameStabilityContainer
{ {

View File

@ -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 System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Utils;
namespace osu.Game.Tests.Mods
{
[TestFixture]
public class MultiModIncompatibilityTest
{
/// <summary>
/// Ensures that all mods grouped into <see cref="MultiMod"/>s, as declared by the default rulesets, are pairwise incompatible with each other.
/// </summary>
[TestCase(typeof(OsuRuleset))]
[TestCase(typeof(TaikoRuleset))]
[TestCase(typeof(CatchRuleset))]
[TestCase(typeof(ManiaRuleset))]
public void TestAllMultiModsFromRulesetAreIncompatible(Type rulesetType)
{
var ruleset = (Ruleset)Activator.CreateInstance(rulesetType);
Assert.That(ruleset, Is.Not.Null);
var allMultiMods = getMultiMods(ruleset);
Assert.Multiple(() =>
{
foreach (var multiMod in allMultiMods)
{
int modCount = multiMod.Mods.Length;
for (int i = 0; i < modCount; ++i)
{
// indexing from i + 1 ensures that only pairs of different mods are checked, and are checked only once
// (indexing from 0 would check each pair twice, and also check each mod against itself).
for (int j = i + 1; j < modCount; ++j)
{
var firstMod = multiMod.Mods[i];
var secondMod = multiMod.Mods[j];
Assert.That(
ModUtils.CheckCompatibleSet(new[] { firstMod, secondMod }), Is.False,
$"{firstMod.Name} ({firstMod.Acronym}) and {secondMod.Name} ({secondMod.Acronym}) should be incompatible.");
}
}
}
});
}
/// <remarks>
/// This local helper is used rather than <see cref="Ruleset.CreateAllMods"/>, because the aforementioned method flattens multi mods.
/// </remarks>>
private static IEnumerable<MultiMod> getMultiMods(Ruleset ruleset)
=> Enum.GetValues(typeof(ModType)).Cast<ModType>().SelectMany(ruleset.GetModsFor).OfType<MultiMod>();
}
}

View File

@ -69,6 +69,34 @@ namespace osu.Game.Tests.NonVisual.Skinning
"Gameplay/osu/followpoint", "Gameplay/osu/followpoint",
"followpoint", 1 "followpoint", 1
}, },
new object[]
{
// Looking up a filename with extension specified should work.
new[] { "followpoint.png" },
"followpoint.png",
"followpoint.png", 1
},
new object[]
{
// Looking up a filename with extension specified should also work with @2x sprites.
new[] { "followpoint@2x.png" },
"followpoint.png",
"followpoint@2x.png", 2
},
new object[]
{
// Looking up a path with extension specified should work.
new[] { "Gameplay/osu/followpoint.png" },
"Gameplay/osu/followpoint.png",
"Gameplay/osu/followpoint.png", 1
},
new object[]
{
// Looking up a path with extension specified should also work with @2x sprites.
new[] { "Gameplay/osu/followpoint@2x.png" },
"Gameplay/osu/followpoint.png",
"Gameplay/osu/followpoint@2x.png", 2
},
}; };
[TestCaseSource(nameof(fallbackTestCases))] [TestCaseSource(nameof(fallbackTestCases))]

View File

@ -33,9 +33,10 @@ namespace osu.Game.Tests.Online
var converted = deserialized?.ToMod(new TestRuleset()); var converted = deserialized?.ToMod(new TestRuleset());
Assert.NotNull(converted);
Assert.That(converted, Is.TypeOf(typeof(UnknownMod))); Assert.That(converted, Is.TypeOf(typeof(UnknownMod)));
Assert.That(converted?.Type, Is.EqualTo(ModType.System)); Assert.That(converted.Type, Is.EqualTo(ModType.System));
Assert.That(converted?.Acronym, Is.EqualTo("WNG??")); Assert.That(converted.Acronym, Is.EqualTo("WNG??"));
} }
[Test] [Test]

View File

@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -110,6 +111,27 @@ namespace osu.Game.Tests.Skins.IO
assertImportedOnce(import1, import2); assertImportedOnce(import1, import2);
}); });
[Test]
public Task TestImportExportedSkinFilename() => runSkinTest(async osu =>
{
MemoryStream exportStream = new MemoryStream();
var import1 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOskWithIni("name 1", "author 1"), "custom.osk"));
assertCorrectMetadata(import1, "name 1 [custom]", "author 1", osu);
import1.PerformRead(s =>
{
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
});
string exportFilename = import1.GetDisplayString();
var import2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(exportStream, $"{exportFilename}.osk"));
assertCorrectMetadata(import2, "name 1 [custom]", "author 1", osu);
assertImportedOnce(import1, import2);
});
[Test] [Test]
public Task TestSameMetadataNameSameFolderName() => runSkinTest(async osu => public Task TestSameMetadataNameSameFolderName() => runSkinTest(async osu =>
{ {

View File

@ -359,9 +359,9 @@ namespace osu.Game.Tests.Visual.Background
protected override BackgroundScreen CreateBackground() => protected override BackgroundScreen CreateBackground() =>
new FadeAccessibleBackground(Beatmap.Value); new FadeAccessibleBackground(Beatmap.Value);
public override void OnEntering(IScreen last) public override void OnEntering(ScreenTransitionEvent e)
{ {
base.OnEntering(last); base.OnEntering(e);
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground)); ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
} }

View File

@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Collections
}); });
Dependencies.Cache(manager); Dependencies.Cache(manager);
Dependencies.Cache(dialogOverlay); Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
} }
[SetUp] [SetUp]

View File

@ -1,44 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene public class TestSceneComposeScreen : EditorClockTestScene
{ {
[Cached(typeof(EditorBeatmap))] private EditorBeatmap editorBeatmap;
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap =
new EditorBeatmap(new OsuBeatmap
{
BeatmapInfo =
{
Ruleset = new OsuRuleset().RulesetInfo
}
});
[Cached] [Cached]
private EditorClipboard clipboard = new EditorClipboard(); private EditorClipboard clipboard = new EditorClipboard();
protected override void LoadComplete() [SetUpSteps]
public void SetUpSteps()
{ {
base.LoadComplete(); AddStep("setup compose screen", () =>
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Child = new ComposeScreen
{ {
State = { Value = Visibility.Visible }, var beatmap = new OsuBeatmap
}; {
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
};
editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null));
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(EditorBeatmap), editorBeatmap),
(typeof(IBeatSnapProvider), editorBeatmap),
},
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
};
});
AddUntilStep("wait for composer", () => this.ChildrenOfType<HitObjectComposer>().SingleOrDefault()?.IsLoaded == true);
}
/// <summary>
/// Ensures that the skin of the edited beatmap is properly wrapped in a <see cref="LegacySkinTransformer"/>.
/// </summary>
[Test]
public void TestLegacyBeatmapSkinHasTransformer()
{
AddAssert("legacy beatmap skin has transformer", () =>
{
var sources = this.ChildrenOfType<BeatmapSkinProvidingContainer>().First().AllSources;
return sources.OfType<LegacySkinTransformer>().Count(t => t.Skin == editorBeatmap.BeatmapSkin.AsNonNull().Skin) == 1;
});
} }
} }
} }

View File

@ -7,11 +7,10 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables; using osu.Game.Storyboards.Drawables;
using osuTK; using osuTK;
@ -36,7 +35,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
assertSpritesFromSkin(false); AddAssert("sprite didn't find texture", () =>
sprites.All(sprite => sprite.ChildrenOfType<Sprite>().All(s => s.Texture == null)));
} }
[Test] [Test]
@ -48,9 +48,12 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
assertSpritesFromSkin(true); // Only checking for at least one sprite that succeeded, as not all skins in this test provide the hitcircleoverlay texture.
AddAssert("sprite found texture", () =>
sprites.Any(sprite => sprite.ChildrenOfType<Sprite>().All(s => s.Texture != null)));
AddAssert("skinnable sprite has correct size", () => sprites.Any(s => Precision.AlmostEquals(s.ChildrenOfType<SkinnableSprite>().Single().Size, new Vector2(128, 128)))); AddAssert("skinnable sprite has correct size", () =>
sprites.Any(sprite => sprite.ChildrenOfType<Sprite>().All(s => s.Size == new Vector2(128))));
} }
[Test] [Test]
@ -104,9 +107,5 @@ namespace osu.Game.Tests.Visual.Gameplay
s.LifetimeStart = double.MinValue; s.LifetimeStart = double.MinValue;
s.LifetimeEnd = double.MaxValue; s.LifetimeEnd = double.MaxValue;
}); });
private void assertSpritesFromSkin(bool fromSkin) =>
AddAssert($"sprites are {(fromSkin ? "from skin" : "from storyboard")}",
() => sprites.All(sprite => sprite.ChildrenOfType<SkinnableSprite>().Any() == fromSkin));
} }
} }

View File

@ -56,10 +56,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private double lastFrequency = double.MaxValue; private double lastFrequency = double.MaxValue;
protected override void Update() protected override void UpdateAfterChildren()
{ {
base.Update(); base.UpdateAfterChildren();
// This must be done in UpdateAfterChildren to allow the gameplay clock to have updated before checking values.
double freq = Beatmap.Value.Track.AggregateFrequency.Value; double freq = Beatmap.Value.Track.AggregateFrequency.Value;
FrequencyIncreased |= freq > lastFrequency; FrequencyIncreased |= freq > lastFrequency;

View File

@ -21,7 +21,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
AddUntilStep("player is playing", () => Player.LocalUserPlaying.Value);
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed); AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddAssert("player is not playing", () => !Player.LocalUserPlaying.Value);
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
AddAssert("total number of results == 1", () => AddAssert("total number of results == 1", () =>
{ {

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("get variables", () => AddUntilStep("get variables", () =>
{ {
sampleDisabler = Player; sampleDisabler = Player;
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).FirstOrDefault(); slider = Player.ChildrenOfType<DrawableSlider>().MinBy(s => s.HitObject.StartTime);
samples = slider?.ChildrenOfType<PoolableSkinnableSample>().ToArray(); samples = slider?.ChildrenOfType<PoolableSkinnableSample>().ToArray();
return slider != null; return slider != null;

View File

@ -1,12 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -36,10 +34,10 @@ namespace osu.Game.Tests.Visual.Gameplay
BeatmapInfo = { AudioLeadIn = leadIn } BeatmapInfo = { AudioLeadIn = leadIn }
}); });
AddAssert($"first frame is {expectedStartTime}", () => AddStep("check first frame time", () =>
{ {
Debug.Assert(player.FirstFrameClockTime != null); Assert.That(player.FirstFrameClockTime, Is.Not.Null);
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
}); });
} }
@ -59,10 +57,10 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
AddAssert($"first frame is {expectedStartTime}", () => AddStep("check first frame time", () =>
{ {
Debug.Assert(player.FirstFrameClockTime != null); Assert.That(player.FirstFrameClockTime, Is.Not.Null);
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
}); });
} }
@ -97,10 +95,10 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
AddAssert($"first frame is {expectedStartTime}", () => AddStep("check first frame time", () =>
{ {
Debug.Assert(player.FirstFrameClockTime != null); Assert.That(player.FirstFrameClockTime, Is.Not.Null);
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
}); });
} }

View File

@ -85,7 +85,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10)));
pauseAndConfirm(); pauseAndConfirm();
AddAssert("player not playing", () => !Player.LocalUserPlaying.Value);
resumeAndConfirm(); resumeAndConfirm();
AddUntilStep("player playing", () => Player.LocalUserPlaying.Value);
} }
[Test] [Test]
@ -389,9 +392,9 @@ namespace osu.Game.Tests.Visual.Gameplay
public void ExitViaQuickExit() => PerformExit(false); public void ExitViaQuickExit() => PerformExit(false);
public override void OnEntering(IScreen last) public override void OnEntering(ScreenTransitionEvent e)
{ {
base.OnEntering(last); base.OnEntering(e);
GameplayClockContainer.Stop(); GameplayClockContainer.Stop();
} }
} }

View File

@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private SessionStatics sessionStatics { get; set; } private SessionStatics sessionStatics { get; set; }
[Cached] [Cached(typeof(INotificationOverlay))]
private readonly NotificationOverlay notificationOverlay; private readonly NotificationOverlay notificationOverlay;
[Cached] [Cached]

View File

@ -26,6 +26,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture] [TestFixture]
public class TestSceneReplayDownloadButton : OsuManualInputManagerTestScene public class TestSceneReplayDownloadButton : OsuManualInputManagerTestScene
{ {
private const long online_score_id = 2553163309;
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
@ -43,6 +45,15 @@ namespace osu.Game.Tests.Visual.Gameplay
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
} }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("delete previous imports", () =>
{
scoreManager.Delete(s => s.OnlineID == online_score_id);
});
}
[Test] [Test]
public void TestDisplayStates() public void TestDisplayStates()
{ {
@ -150,10 +161,12 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true))); AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true)));
AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable); AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable);
AddAssert("button is enabled", () => downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
AddStep("delete score", () => scoreManager.Delete(imported.Value)); AddStep("delete score", () => scoreManager.Delete(imported.Value));
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
} }
[Test] [Test]
@ -178,7 +191,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
return new APIScore return new APIScore
{ {
OnlineID = 2553163309, OnlineID = online_score_id,
RulesetID = 0, RulesetID = 0,
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(), Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
HasReplay = replayAvailable, HasReplay = replayAvailable,

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestSkinSourceContainer skinSource; private TestSkinSourceContainer skinSource;
private PausableSkinnableSound skinnableSound; private PausableSkinnableSound skinnableSound;
[SetUp] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("setup hierarchy", () => AddStep("setup hierarchy", () =>

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Menus
private IntroScreen intro; private IntroScreen intro;
[Cached] [Cached(typeof(INotificationOverlay))]
private NotificationOverlay notifications; private NotificationOverlay notifications;
private ScheduledDelegate trackResetDelegate; private ScheduledDelegate trackResetDelegate;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -22,6 +23,17 @@ namespace osu.Game.Tests.Visual.Menus
[Resolved] [Resolved]
private IRulesetStore rulesets { get; set; } private IRulesetStore rulesets { get; set; }
private readonly Mock<INotificationOverlay> notifications = new Mock<INotificationOverlay>();
private readonly BindableInt unreadNotificationCount = new BindableInt();
[BackgroundDependencyLoader]
private void load()
{
Dependencies.CacheAs(notifications.Object);
notifications.SetupGet(n => n.UnreadCount).Returns(unreadNotificationCount);
}
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
@ -31,10 +43,6 @@ namespace osu.Game.Tests.Visual.Menus
[Test] [Test]
public void TestNotificationCounter() public void TestNotificationCounter()
{ {
ToolbarNotificationButton notificationButton = null;
AddStep("retrieve notification button", () => notificationButton = toolbar.ChildrenOfType<ToolbarNotificationButton>().Single());
setNotifications(1); setNotifications(1);
setNotifications(2); setNotifications(2);
setNotifications(3); setNotifications(3);
@ -43,7 +51,7 @@ namespace osu.Game.Tests.Visual.Menus
void setNotifications(int count) void setNotifications(int count)
=> AddStep($"set notification count to {count}", => AddStep($"set notification count to {count}",
() => notificationButton.NotificationCount.Value = count); () => unreadNotificationCount.Value = count);
} }
[TestCase(false)] [TestCase(false)]

View File

@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -15,7 +18,10 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture] [TestFixture]
public class TestSceneToolbarClock : OsuManualInputManagerTestScene public class TestSceneToolbarClock : OsuManualInputManagerTestScene
{ {
private Bindable<ToolbarClockDisplayMode> clockDisplayMode;
private readonly Container mainContainer; private readonly Container mainContainer;
private readonly ToolbarClock toolbarClock;
public TestSceneToolbarClock() public TestSceneToolbarClock()
{ {
@ -49,7 +55,7 @@ namespace osu.Game.Tests.Visual.Menus
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = 2, Width = 2,
}, },
new ToolbarClock(), toolbarClock = new ToolbarClock(),
new Box new Box
{ {
Colour = Color4.DarkRed, Colour = Color4.DarkRed,
@ -65,6 +71,12 @@ namespace osu.Game.Tests.Visual.Menus
AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale)); AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale));
} }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
clockDisplayMode = config.GetBindable<ToolbarClockDisplayMode>(OsuSetting.ToolbarClockDisplayMode);
}
[Test] [Test]
public void TestRealGameTime() public void TestRealGameTime()
{ {
@ -76,5 +88,20 @@ namespace osu.Game.Tests.Visual.Menus
{ {
AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 }); AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 });
} }
[Test]
public void TestDisplayModeChange()
{
AddStep("Set clock display mode", () => clockDisplayMode.Value = ToolbarClockDisplayMode.Full);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is digital with runtime", () => clockDisplayMode.Value == ToolbarClockDisplayMode.DigitalWithRuntime);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is digital", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Digital);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is analog", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Analog);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is full", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Full);
}
} }
} }

View File

@ -0,0 +1,210 @@
// 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.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Multiplayer
{
public abstract class MultiplayerGameplayLeaderboardTestScene : OsuTestScene
{
private const int total_users = 16;
protected readonly BindableList<MultiplayerRoomUser> MultiplayerUsers = new BindableList<MultiplayerRoomUser>();
protected MultiplayerGameplayLeaderboard Leaderboard { get; private set; }
protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId);
protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor);
private readonly BindableList<int> multiplayerUserIds = new BindableList<int>();
private OsuConfigManager config;
private readonly Mock<SpectatorClient> spectatorClient = new Mock<SpectatorClient>();
private readonly Mock<MultiplayerClient> multiplayerClient = new Mock<MultiplayerClient>();
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
[BackgroundDependencyLoader]
private void load()
{
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
Dependencies.CacheAs(spectatorClient.Object);
Dependencies.CacheAs(multiplayerClient.Object);
// To emulate `MultiplayerClient.CurrentMatchPlayingUserIds` we need a bindable list of *only IDs*.
// This tracks the list of users 1:1.
MultiplayerUsers.BindCollectionChanged((c, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Debug.Assert(e.NewItems != null);
foreach (var user in e.NewItems.OfType<MultiplayerRoomUser>())
multiplayerUserIds.Add(user.UserID);
break;
case NotifyCollectionChangedAction.Remove:
Debug.Assert(e.OldItems != null);
foreach (var user in e.OldItems.OfType<MultiplayerRoomUser>())
multiplayerUserIds.Remove(user.UserID);
break;
case NotifyCollectionChangedAction.Reset:
multiplayerUserIds.Clear();
break;
}
});
multiplayerClient.SetupGet(c => c.CurrentMatchPlayingUserIds)
.Returns(() => multiplayerUserIds);
}
[SetUpSteps]
public virtual void SetUpSteps()
{
AddStep("reset counts", () =>
{
spectatorClient.Invocations.Clear();
lastHeaders.Clear();
});
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = new APIUser
{
Id = 1,
});
AddStep("populate users", () =>
{
MultiplayerUsers.Clear();
for (int i = 0; i < total_users; i++)
MultiplayerUsers.Add(CreateUser(i));
});
AddStep("create leaderboard", () =>
{
Leaderboard?.Expire();
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
OsuScoreProcessor scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playableBeatmap);
Child = scoreProcessor;
LoadComponentAsync(Leaderboard = CreateLeaderboard(scoreProcessor), Add);
});
AddUntilStep("wait for load", () => Leaderboard.IsLoaded);
AddStep("check watch requests were sent", () =>
{
foreach (var user in MultiplayerUsers)
spectatorClient.Verify(s => s.WatchUser(user.UserID), Times.Once);
});
}
[Test]
public void TestScoreUpdates()
{
AddRepeatStep("update state", UpdateUserStatesRandomly, 100);
AddToggleStep("switch compact mode", expanded => Leaderboard.Expanded.Value = expanded);
}
[Test]
public void TestUserQuit()
{
AddUntilStep("mark users quit", () =>
{
if (MultiplayerUsers.Count == 0)
return true;
MultiplayerUsers.RemoveAt(0);
return false;
});
AddStep("check stop watching requests were sent", () =>
{
foreach (var user in MultiplayerUsers)
spectatorClient.Verify(s => s.StopWatchingUser(user.UserID), Times.Once);
});
}
[Test]
public void TestChangeScoringMode()
{
AddRepeatStep("update state", UpdateUserStatesRandomly, 5);
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
}
protected void UpdateUserStatesRandomly()
{
foreach (var user in MultiplayerUsers)
{
if (RNG.NextBool())
continue;
int userId = user.UserID;
if (!lastHeaders.TryGetValue(userId, out var header))
{
lastHeaders[userId] = header = new FrameHeader(new ScoreInfo
{
Statistics = new Dictionary<HitResult, int>
{
[HitResult.Miss] = 0,
[HitResult.Meh] = 0,
[HitResult.Great] = 0
}
});
}
switch (RNG.Next(0, 3))
{
case 0:
header.Combo = 0;
header.Statistics[HitResult.Miss]++;
break;
case 1:
header.Combo++;
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
header.Statistics[HitResult.Meh]++;
break;
default:
header.Combo++;
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
header.Statistics[HitResult.Great]++;
break;
}
spectatorClient.Raise(s => s.OnNewFrames -= null, userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) }));
}
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
@ -34,9 +35,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestMultipleStatuses() public void TestMultipleStatuses()
{ {
FillFlowContainer rooms = null;
AddStep("create rooms", () => AddStep("create rooms", () =>
{ {
Child = new FillFlowContainer Child = rooms = new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -124,6 +127,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
}; };
}); });
AddUntilStep("wait for panel load", () => rooms.Count == 5);
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2);
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 3);
} }
[Test] [Test]

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneFreeModSelectScreen : MultiplayerTestScene
{
[Test]
public void TestFreeModSelect()
{
FreeModSelectScreen freeModSelectScreen = null;
AddStep("create free mod select screen", () => Child = freeModSelectScreen = new FreeModSelectScreen
{
State = { Value = Visibility.Visible }
});
AddUntilStep("all column content loaded",
() => freeModSelectScreen.ChildrenOfType<ModColumn>().Any()
&& freeModSelectScreen.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
AddUntilStep("all visible mods are playable",
() => this.ChildrenOfType<ModPanel>()
.Where(panel => panel.IsPresent)
.All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
AddToggleStep("toggle visibility", visible =>
{
if (freeModSelectScreen != null)
freeModSelectScreen.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
});
}
}
}

View File

@ -9,13 +9,14 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneGameplayChatDisplay : MultiplayerTestScene public class TestSceneGameplayChatDisplay : OsuManualInputManagerTestScene
{ {
private GameplayChatDisplay chatDisplay; private GameplayChatDisplay chatDisplay;
@ -35,11 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public void SetUpSteps()
{ {
base.SetUpSteps(); AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay(new Room())
AddStep("load chat display", () => Child = chatDisplay = new GameplayChatDisplay(SelectedRoom.Value)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -3,80 +3,155 @@
using System; using System;
using System.Linq; using System.Linq;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform; using osu.Framework.Logging;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMatchStartControl : MultiplayerTestScene public class TestSceneMatchStartControl : OsuManualInputManagerTestScene
{ {
private readonly Mock<MultiplayerClient> multiplayerClient = new Mock<MultiplayerClient>();
private readonly Mock<OnlinePlayBeatmapAvailabilityTracker> availabilityTracker = new Mock<OnlinePlayBeatmapAvailabilityTracker>();
private readonly Bindable<BeatmapAvailability> beatmapAvailability = new Bindable<BeatmapAvailability>();
private readonly Bindable<Room> room = new Bindable<Room>();
private MultiplayerRoom multiplayerRoom;
private MultiplayerRoomUser localUser;
private OngoingOperationTracker ongoingOperationTracker;
private PopoverContainer content;
private MatchStartControl control; private MatchStartControl control;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>(); private OsuButton readyButton => control.ChildrenOfType<OsuButton>().Single();
private BeatmapManager beatmaps; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
private RulesetStore rulesets; new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } };
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load()
{ {
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.CacheAs(multiplayerClient.Object);
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker());
Dependencies.Cache(Realm); Dependencies.CacheAs(availabilityTracker.Object);
availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
// By default, the local user is to be the host.
multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
// Assume all state changes are accepted by the server.
multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
.Callback((MultiplayerUserState r) =>
{
Logger.Log($"Changing local user state from {localUser.State} to {r}");
localUser.State = r;
raiseRoomUpdated();
});
multiplayerClient.Setup(m => m.StartMatch())
.Callback(() =>
{
multiplayerClient.Raise(m => m.LoadRequested -= null);
// immediately "end" gameplay, as we don't care about that part of the process.
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
});
multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
.Callback((MatchUserRequest request) =>
{
switch (request)
{
case StartMatchCountdownRequest countdownStart:
setRoomCountdown(countdownStart.Duration);
break;
case StopCountdownRequest _:
multiplayerRoom.Countdown = null;
raiseRoomUpdated();
break;
}
});
Children = new Drawable[]
{
ongoingOperationTracker,
content = new PopoverContainer { RelativeSizeAxes = Axes.Both }
};
} }
[SetUp] [SetUpSteps]
public new void Setup() => Schedule(() => public void SetUpSteps()
{ {
AvailabilityTracker.SelectedItem.BindTo(selectedItem); AddStep("reset state", () =>
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{ {
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID multiplayerClient.Invocations.Clear();
};
Child = new PopoverContainer beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
var playlistItem = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
room.Value = new Room
{
Playlist = { playlistItem },
CurrentPlaylistItem = { Value = playlistItem }
};
localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id) { User = API.LocalUser.Value };
multiplayerRoom = new MultiplayerRoom(0)
{
Playlist =
{
new MultiplayerPlaylistItem(playlistItem),
},
Users = { localUser },
Host = localUser,
};
});
AddStep("create control", () =>
{ {
RelativeSizeAxes = Axes.Both, content.Child = control = new MatchStartControl
Child = control = new MatchStartControl
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(250, 50), Size = new Vector2(250, 50),
} };
}; });
}); }
[Test] [Test]
public void TestStartWithCountdown() public void TestStartWithCountdown()
{ {
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true); AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>(); ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () => AddStep("click the first countdown button", () =>
{ {
@ -85,8 +160,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddStep("check request received", () =>
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); {
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
req.Duration == TimeSpan.FromSeconds(10)
)), Times.Once);
});
} }
[Test] [Test]
@ -94,6 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true); AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>(); ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () => AddStep("click the first countdown button", () =>
{ {
@ -102,6 +182,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddStep("check request received", () =>
{
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
req.Duration == TimeSpan.FromSeconds(10)
)), Times.Once);
});
ClickButtonWhenEnabled<MultiplayerCountdownButton>(); ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the cancel button", () => AddStep("click the cancel button", () =>
{ {
@ -110,41 +197,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddStep("check request received", () =>
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); {
multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny<StopCountdownRequest>()), Times.Once);
});
} }
[Test] [Test]
public void TestReadyAndUnReadyDuringCountdown() public void TestReadyAndUnReadyDuringCountdown()
{ {
AddStep("add second user as host", () => AddStep("add second user as host", () => addUser(new APIUser { Id = 2, Username = "Another user" }, true));
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely()); AddStep("start countdown", () => setRoomCountdown(TimeSpan.FromMinutes(1)));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
} }
[Test] [Test]
public void TestCountdownWhileSpectating() public void TestCountdownWhileSpectating()
{ {
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddStep("set spectating", () => changeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); checkLocalUserState(MultiplayerUserState.Spectating);
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); AddStep("add second user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); AddStep("set second user ready", () => changeUserState(2, MultiplayerUserState.Ready));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
} }
@ -153,60 +238,54 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("add second user as host", () => AddStep("add second user as host", () =>
{ {
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); addUser(new APIUser { Id = 2, Username = "Another user" }, true);
MultiplayerClient.TransferHost(2);
}); });
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("start countdown", () => multiplayerClient.Object.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); AddUntilStep("countdown started", () => multiplayerRoom.Countdown != null);
AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); AddStep("transfer host to local user", () => transferHost(localUser));
AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true); AddUntilStep("local user is host", () => multiplayerRoom.Host?.Equals(multiplayerClient.Object.LocalUser) == true);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); AddAssert("countdown still active", () => multiplayerRoom.Countdown != null);
} }
[Test] [Test]
public void TestCountdownButtonVisibilityWithAutoStartEnablement() public void TestCountdownButtonVisibilityWithAutoStart()
{ {
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
} }
[Test] [Test]
public void TestClickingReadyButtonUnReadiesDuringAutoStart() public void TestClickingReadyButtonUnReadiesDuringAutoStart()
{ {
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
} }
[Test] [Test]
public void TestDeletedBeatmapDisableReady() public void TestDeletedBeatmapDisableReady()
{ {
OsuButton readyButton = null; AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
AddUntilStep("ensure ready button enabled", () => AddStep("mark beatmap not available", () => beatmapAvailability.Value = BeatmapAvailability.NotDownloaded());
{
readyButton = control.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value); AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddStep("mark beatmap available", () => beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable());
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value); AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
} }
@ -215,31 +294,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("add second user as host", () => AddStep("add second user as host", () =>
{ {
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); addUser(new APIUser { Id = 2, Username = "Another user" }, true);
MultiplayerClient.TransferHost(2);
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
} }
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public void TestToggleStateWhenHost(bool allReady) public void TestToggleStateWhenHost(bool allReady)
{ {
AddStep("setup", () => if (!allReady)
{ AddStep("add other user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
if (!allReady)
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
verifyGameplayStartFlow(); verifyGameplayStartFlow();
} }
@ -249,12 +322,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("add host", () => AddStep("add host", () =>
{ {
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); addUser(new APIUser { Id = 2, Username = "Another user" }, true);
MultiplayerClient.TransferHost(2);
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
AddStep("make local user host", () => transferHost(localUser));
verifyGameplayStartFlow(); verifyGameplayStartFlow();
} }
@ -264,18 +337,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("setup", () => AddStep("setup", () =>
{ {
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0); addUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); AddStep("transfer host", () => transferHost(multiplayerRoom.Users[1]));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value); AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
} }
[TestCase(true)] [TestCase(true)]
@ -283,44 +355,83 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestManyUsersChangingState(bool isHost) public void TestManyUsersChangingState(bool isHost)
{ {
const int users = 10; const int users = 10;
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
for (int i = 0; i < users; i++)
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
});
if (!isHost) AddStep("add many users", () =>
AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); {
for (int i = 0; i < users; i++)
addUser(new APIUser { Id = i, Username = "Another user" }, !isHost && i == 2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddRepeatStep("change user ready state", () => AddRepeatStep("change user ready state", () =>
{ {
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle); changeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
}, 20); }, 20);
AddRepeatStep("ready all users", () => AddRepeatStep("ready all users", () =>
{ {
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); var nextUnready = multiplayerRoom.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null) if (nextUnready != null)
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); changeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
}, users); }, users);
} }
private void verifyGameplayStartFlow() private void verifyGameplayStartFlow()
{ {
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddStep("finish gameplay", () => AddStep("check start request received", () => multiplayerClient.Verify(m => m.StartMatch(), Times.Once));
{
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
});
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
} }
private void checkLocalUserState(MultiplayerUserState state) =>
AddUntilStep($"local user is {state}", () => localUser.State == state);
private void setRoomCountdown(TimeSpan duration)
{
multiplayerRoom.Countdown = new MatchStartCountdown { TimeRemaining = duration };
raiseRoomUpdated();
}
private void changeUserState(int userId, MultiplayerUserState newState)
{
multiplayerRoom.Users.Single(u => u.UserID == userId).State = newState;
raiseRoomUpdated();
}
private void addUser(APIUser user, bool asHost = false)
{
var multiplayerRoomUser = new MultiplayerRoomUser(user.Id) { User = user };
multiplayerRoom.Users.Add(multiplayerRoomUser);
if (asHost)
transferHost(multiplayerRoomUser);
raiseRoomUpdated();
}
private void transferHost(MultiplayerRoomUser user)
{
multiplayerRoom.Host = user;
raiseRoomUpdated();
}
private void changeRoomSettings(MultiplayerRoomSettings settings)
{
multiplayerRoom.Settings = settings;
// Changing settings should reset all user ready statuses.
foreach (var user in multiplayerRoom.Users)
{
if (user.State == MultiplayerUserState.Ready)
user.State = MultiplayerUserState.Idle;
}
raiseRoomUpdated();
}
private void raiseRoomUpdated() => multiplayerClient.Raise(m => m.RoomUpdated -= null);
} }
} }

View File

@ -464,16 +464,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
private class TestMultiSpectatorScreen : MultiSpectatorScreen private class TestMultiSpectatorScreen : MultiSpectatorScreen
{ {
private readonly double? gameplayStartTime; private readonly double? startTime;
public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? gameplayStartTime = null) public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? startTime = null)
: base(room, users) : base(room, users)
{ {
this.gameplayStartTime = gameplayStartTime; this.startTime = startTime;
} }
protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap)
=> new MasterGameplayClockContainer(beatmap, gameplayStartTime ?? 0, gameplayStartTime.HasValue); => new MasterGameplayClockContainer(beatmap, 0) { StartTime = startTime ?? 0 };
} }
} }
} }

View File

@ -495,17 +495,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true); AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddAssert("Mods match current item",
() => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() }); AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() });
AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddAssert("Mods don't match current item",
() => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddAssert("Mods match current item",
() => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
} }
[Test] [Test]
@ -665,6 +668,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
} }
[Test]
public void TestGameplayDoesntStartWithNonLoadedUser()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
}
}
});
pressReadyButton();
AddStep("join other user and ready", () =>
{
multiplayerClient.AddUser(new APIUser { Id = 1234 });
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
});
AddStep("start match", () =>
{
multiplayerClient.StartMatch();
});
AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player);
AddWaitStep("wait some", 20);
AddAssert("ensure gameplay hasn't started", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.IsRunning == false);
}
[Test] [Test]
public void TestRoomSettingsReQueriedWhenJoiningRoom() public void TestRoomSettingsReQueriedWhenJoiningRoom()
{ {

View File

@ -1,161 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerGameplayLeaderboardTestScene
{ {
private static IEnumerable<int> users => Enumerable.Range(0, 16); protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor)
public new TestMultiplayerSpectatorClient SpectatorClient => (TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient;
private MultiplayerGameplayLeaderboard leaderboard;
private OsuConfigManager config;
[BackgroundDependencyLoader]
private void load()
{ {
Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); return new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely());
AddStep("create leaderboard", () =>
{ {
leaderboard?.Expire(); Anchor = Anchor.Centre,
Origin = Anchor.Centre,
OsuScoreProcessor scoreProcessor; };
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var multiplayerUsers = new List<MultiplayerRoomUser>();
foreach (int user in users)
{
SpectatorClient.SendStartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID);
multiplayerUsers.Add(OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true));
}
Children = new Drawable[]
{
scoreProcessor = new OsuScoreProcessor(),
};
scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}, Add);
});
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
AddUntilStep("wait for user population", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count > 0);
}
[Test]
public void TestScoreUpdates()
{
AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
}
[Test]
public void TestUserQuit()
{
foreach (int user in users)
AddStep($"mark user {user} quit", () => MultiplayerClient.RemoveUser(UserLookupCache.GetUserAsync(user).GetResultSafely().AsNonNull()));
}
[Test]
public void TestChangeScoringMode()
{
AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 5);
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
}
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
protected class TestDependencies : MultiplayerTestSceneDependencies
{
protected override TestSpectatorClient CreateSpectatorClient() => new TestMultiplayerSpectatorClient();
}
public class TestMultiplayerSpectatorClient : TestSpectatorClient
{
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
public void RandomlyUpdateState()
{
foreach ((int userId, _) in WatchedUserStates)
{
if (RNG.NextBool())
continue;
if (!lastHeaders.TryGetValue(userId, out var header))
{
lastHeaders[userId] = header = new FrameHeader(new ScoreInfo
{
Statistics = new Dictionary<HitResult, int>
{
[HitResult.Miss] = 0,
[HitResult.Meh] = 0,
[HitResult.Great] = 0
}
});
}
switch (RNG.Next(0, 3))
{
case 0:
header.Combo = 0;
header.Statistics[HitResult.Miss]++;
break;
case 1:
header.Combo++;
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
header.Statistics[HitResult.Meh]++;
break;
default:
header.Combo++;
header.MaxCombo = Math.Max(header.MaxCombo, header.Combo);
header.Statistics[HitResult.Great]++;
break;
}
((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) }));
}
}
} }
} }
} }

View File

@ -1,121 +1,57 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus; using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerTestScene public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerGameplayLeaderboardTestScene
{ {
private static IEnumerable<int> users => Enumerable.Range(0, 16); private int team;
public new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient SpectatorClient => protected override MultiplayerRoomUser CreateUser(int userId)
(TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient;
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
protected class TestDependencies : MultiplayerTestSceneDependencies
{ {
protected override TestSpectatorClient CreateSpectatorClient() => new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient(); var user = base.CreateUser(userId);
user.MatchState = new TeamVersusUserState
{
TeamID = team++ % 2
};
return user;
} }
private MultiplayerGameplayLeaderboard leaderboard; protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor) =>
private GameplayMatchScoreDisplay gameplayScoreDisplay; new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
{
protected override Room CreateRoom() Anchor = Anchor.Centre,
{ Origin = Anchor.Centre,
var room = base.CreateRoom(); };
room.Type.Value = MatchType.TeamVersus;
return room;
}
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps(); base.SetUpSteps();
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = UserLookupCache.GetUserAsync(1).GetResultSafely()); AddStep("Add external display components", () =>
AddStep("create leaderboard", () =>
{ {
leaderboard?.Expire(); LoadComponentAsync(new MatchScoreDisplay
OsuScoreProcessor scoreProcessor;
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var multiplayerUsers = new List<MultiplayerRoomUser>();
foreach (int user in users)
{ {
SpectatorClient.SendStartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID); Team1Score = { BindTarget = Leaderboard.TeamScores[0] },
var roomUser = OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = user }, true); Team2Score = { BindTarget = Leaderboard.TeamScores[1] }
}, Add);
roomUser.MatchState = new TeamVersusUserState LoadComponentAsync(new GameplayMatchScoreDisplay
{
TeamID = RNG.Next(0, 2)
};
multiplayerUsers.Add(roomUser);
}
Children = new Drawable[]
{ {
scoreProcessor = new OsuScoreProcessor(), Anchor = Anchor.BottomCentre,
}; Origin = Anchor.BottomCentre,
Team1Score = { BindTarget = Leaderboard.TeamScores[0] },
scoreProcessor.ApplyBeatmap(playableBeatmap); Team2Score = { BindTarget = Leaderboard.TeamScores[1] },
Expanded = { BindTarget = Leaderboard.Expanded },
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray()) }, Add);
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}, gameplayLeaderboard =>
{
LoadComponentAsync(new MatchScoreDisplay
{
Team1Score = { BindTarget = leaderboard.TeamScores[0] },
Team2Score = { BindTarget = leaderboard.TeamScores[1] }
}, Add);
LoadComponentAsync(gameplayScoreDisplay = new GameplayMatchScoreDisplay
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Team1Score = { BindTarget = leaderboard.TeamScores[0] },
Team2Score = { BindTarget = leaderboard.TeamScores[1] }
}, Add);
Add(gameplayLeaderboard);
});
});
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
AddUntilStep("wait for user population", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count > 0);
}
[Test]
public void TestScoreUpdates()
{
AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded =>
{
leaderboard.Expanded.Value = expanded;
gameplayScoreDisplay.Expanded.Value = expanded;
}); });
} }
} }

View File

@ -4,6 +4,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
@ -13,13 +14,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp] [SetUp]
public new void Setup() => Schedule(() => public new void Setup() => Schedule(() =>
{ {
Child = new Container Child = new PopoverContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.Both,
Height = 50, Child = new Container
Child = new MultiplayerMatchFooter() {
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 50,
Child = new MultiplayerMatchFooter()
}
}; };
}); });
} }

View File

@ -8,13 +8,12 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -27,6 +26,7 @@ using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -35,10 +35,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager manager; private BeatmapManager manager;
private RulesetStore rulesets; private RulesetStore rulesets;
private List<BeatmapInfo> beatmaps; private IList<BeatmapInfo> beatmaps => importedBeatmapSet?.PerformRead(s => s.Beatmaps) ?? new List<BeatmapInfo>();
private TestMultiplayerMatchSongSelect songSelect; private TestMultiplayerMatchSongSelect songSelect;
private Live<BeatmapSetInfo> importedBeatmapSet;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
@ -46,44 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
beatmaps = new List<BeatmapInfo>(); importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()));
var metadata = new BeatmapMetadata
{
Artist = "Some Artist",
Title = "Some Beatmap",
Author = { Username = "Some Author" },
};
var beatmapSetInfo = new BeatmapSetInfo
{
OnlineID = 10,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
DateAdded = DateTimeOffset.UtcNow
};
for (int i = 0; i < 8; ++i)
{
int beatmapId = 10 * 10 + i;
int length = RNG.Next(30000, 200000);
double bpm = RNG.NextSingle(80, 200);
var beatmap = new BeatmapInfo
{
Ruleset = rulesets.GetRuleset(i % 4) ?? throw new InvalidOperationException(),
OnlineID = beatmapId,
Length = length,
BPM = bpm,
Metadata = metadata,
Difficulty = new BeatmapDifficulty()
};
beatmaps.Add(beatmap);
beatmapSetInfo.Beatmaps.Add(beatmap);
}
manager.Import(beatmapSetInfo);
} }
public override void SetUpSteps() public override void SetUpSteps()

View File

@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -183,14 +184,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertItemInHistoryListStep(2, 0); assertItemInHistoryListStep(2, 0);
} }
[Test]
public void TestInsertedItemDoesNotRefreshAllOthers()
{
AddStep("change to round robin queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayersRoundRobin }).WaitSafely());
// Add a few items for the local user.
addItemStep();
addItemStep();
addItemStep();
addItemStep();
addItemStep();
DrawableRoomPlaylistItem[] drawableItems = null;
AddStep("get drawable items", () => drawableItems = this.ChildrenOfType<DrawableRoomPlaylistItem>().ToArray());
// Add 1 item for another user.
AddStep("join second user", () => MultiplayerClient.AddUser(new APIUser { Id = 10 }));
addItemStep(userId: 10);
// New item inserted towards the top of the list.
assertItemInQueueListStep(7, 1);
AddAssert("all previous playlist items remained", () => drawableItems.All(this.ChildrenOfType<DrawableRoomPlaylistItem>().Contains));
}
/// <summary> /// <summary>
/// Adds a step to create a new playlist item. /// Adds a step to create a new playlist item.
/// </summary> /// </summary>
private void addItemStep(bool expired = false) => AddStep("add item", () => MultiplayerClient.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{ {
Expired = expired, MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
PlayedAt = DateTimeOffset.Now {
}))); Expired = expired,
PlayedAt = DateTimeOffset.Now
})).WaitSafely();
});
/// <summary> /// <summary>
/// Asserts the position of a given playlist item in the queue list. /// Asserts the position of a given playlist item in the queue list.

View File

@ -26,10 +26,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
var score = TestResources.CreateTestScoreInfo(beatmapInfo); var score = TestResources.CreateTestScoreInfo(beatmapInfo);
SortedDictionary<int, BindableInt> teamScores = new SortedDictionary<int, BindableInt> SortedDictionary<int, BindableLong> teamScores = new SortedDictionary<int, BindableLong>
{ {
{ 0, new BindableInt(team1Score) }, { 0, new BindableLong(team1Score) },
{ 1, new BindableInt(team2Score) } { 1, new BindableLong(team2Score) }
}; };
Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, new PlaylistItem(beatmapInfo), teamScores)); Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, new PlaylistItem(beatmapInfo), teamScores));

View File

@ -1,67 +1,68 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneRankRangePill : MultiplayerTestScene public class TestSceneRankRangePill : OsuTestScene
{ {
[SetUp] private readonly Mock<MultiplayerClient> multiplayerClient = new Mock<MultiplayerClient>();
public new void Setup() => Schedule(() =>
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
// not used directly in component, but required due to it inheriting from OnlinePlayComposite.
new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
private void load()
{ {
Dependencies.CacheAs(multiplayerClient.Object);
Child = new RankRangePill Child = new RankRangePill
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre
}; };
}); }
[Test] [Test]
public void TestSingleUser() public void TestSingleUser()
{ {
AddStep("add user", () => setupRoomWithUsers(new APIUser
{ {
MultiplayerClient.AddUser(new APIUser Id = 2,
{ Statistics = { GlobalRank = 1234 }
Id = 2,
Statistics = { GlobalRank = 1234 }
});
// Remove the local user so only the one above is displayed.
MultiplayerClient.RemoveUser(API.LocalUser.Value);
}); });
} }
[Test] [Test]
public void TestMultipleUsers() public void TestMultipleUsers()
{ {
AddStep("add users", () => setupRoomWithUsers(
{ new APIUser
MultiplayerClient.AddUser(new APIUser
{ {
Id = 2, Id = 2,
Statistics = { GlobalRank = 1234 } Statistics = { GlobalRank = 1234 }
}); },
new APIUser
MultiplayerClient.AddUser(new APIUser
{ {
Id = 3, Id = 3,
Statistics = { GlobalRank = 3333 } Statistics = { GlobalRank = 3333 }
}); },
new APIUser
MultiplayerClient.AddUser(new APIUser
{ {
Id = 4, Id = 4,
Statistics = { GlobalRank = 4321 } Statistics = { GlobalRank = 4321 }
}); });
// Remove the local user so only the ones above are displayed.
MultiplayerClient.RemoveUser(API.LocalUser.Value);
});
} }
[TestCase(1, 10)] [TestCase(1, 10)]
@ -73,22 +74,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(1000000, 10000000)] [TestCase(1000000, 10000000)]
public void TestRange(int min, int max) public void TestRange(int min, int max)
{ {
AddStep("add users", () => setupRoomWithUsers(
{ new APIUser
MultiplayerClient.AddUser(new APIUser
{ {
Id = 2, Id = 2,
Statistics = { GlobalRank = min } Statistics = { GlobalRank = min }
}); },
new APIUser
MultiplayerClient.AddUser(new APIUser
{ {
Id = 3, Id = 3,
Statistics = { GlobalRank = max } Statistics = { GlobalRank = max }
}); });
}
// Remove the local user so only the ones above are displayed. private void setupRoomWithUsers(params APIUser[] users)
MultiplayerClient.RemoveUser(API.LocalUser.Value); {
AddStep("setup room", () =>
{
multiplayerClient.SetupGet(m => m.Room).Returns(new MultiplayerRoom(0)
{
Users = new List<MultiplayerRoomUser>(users.Select(apiUser => new MultiplayerRoomUser(apiUser.Id) { User = apiUser }))
});
multiplayerClient.Raise(m => m.RoomUpdated -= null);
}); });
} }
} }

View File

@ -0,0 +1,46 @@
// 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.Screens.Menu;
using osu.Game.Screens.Select;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation
{
public class TestSceneButtonSystemNavigation : OsuGameTestScene
{
private ButtonSystem buttons => ((MainMenu)Game.ScreenStack.CurrentScreen).ChildrenOfType<ButtonSystem>().Single();
[Test]
public void TestGlobalActionHasPriority()
{
AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial);
// triggering the cookie in the initial state with any key should only happen if no other action is bound to that key.
// here, F10 is bound to GlobalAction.ToggleGameplayMouseButtons.
AddStep("press F10", () => InputManager.Key(Key.F10));
AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial);
AddStep("press P", () => InputManager.Key(Key.P));
AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel);
}
[Test]
public void TestShortcutKeys()
{
AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial);
AddStep("press P", () => InputManager.Key(Key.P));
AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel);
AddStep("press P", () => InputManager.Key(Key.P));
AddAssert("state is play", () => buttons.State == ButtonSystemState.Play);
AddStep("press P", () => InputManager.Key(Key.P));
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
}
}
}

View File

@ -59,8 +59,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () => AddUntilStep("wait for player", () =>
{ {
// dismiss any notifications that may appear (ie. muted notification). DismissAnyNotifications();
clickMouseInCentre();
return player != null; return player != null;
}); });
@ -73,12 +72,6 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("key counter did increase", () => keyCounter.CountPresses == 1); AddAssert("key counter did increase", () => keyCounter.CountPresses == 1);
} }
private void clickMouseInCentre()
{
InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
}
private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel private KeyBindingsSubsection osuBindingSubsection => keyBindingPanel
.ChildrenOfType<VariantBindingsSubsection>() .ChildrenOfType<VariantBindingsSubsection>()
.FirstOrDefault(s => s.Ruleset.ShortName == "osu"); .FirstOrDefault(s => s.Ruleset.ShortName == "osu");

View File

@ -89,18 +89,11 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () => AddUntilStep("wait for player", () =>
{ {
// dismiss any notifications that may appear (ie. muted notification). DismissAnyNotifications();
clickMouseInCentre();
return (player = Game.ScreenStack.CurrentScreen as Player) != null; return (player = Game.ScreenStack.CurrentScreen as Player) != null;
}); });
AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value); AddUntilStep("wait for play time active", () => !player.IsBreakTime.Value);
} }
private void clickMouseInCentre()
{
InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
}
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Navigation
typeof(OsuLogo), typeof(OsuLogo),
typeof(IdleTracker), typeof(IdleTracker),
typeof(OnScreenDisplay), typeof(OnScreenDisplay),
typeof(NotificationOverlay), typeof(INotificationOverlay),
typeof(BeatmapListingOverlay), typeof(BeatmapListingOverlay),
typeof(DashboardOverlay), typeof(DashboardOverlay),
typeof(NewsOverlay), typeof(NewsOverlay),
@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Navigation
typeof(LoginOverlay), typeof(LoginOverlay),
typeof(MusicController), typeof(MusicController),
typeof(AccountCreationOverlay), typeof(AccountCreationOverlay),
typeof(DialogOverlay), typeof(IDialogOverlay),
typeof(ScreenshotManager) typeof(ScreenshotManager)
}; };

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -113,12 +114,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("did not perform", () => !actionPerformed); AddAssert("did not perform", () => !actionPerformed);
AddAssert("only one exit attempt", () => blocker.ExitAttempts == 1); AddAssert("only one exit attempt", () => blocker.ExitAttempts == 1);
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded); waitForDialogOverlayLoad();
if (confirmed) if (confirmed)
{ {
AddStep("accept dialog", () => InputManager.Key(Key.Number1)); AddStep("accept dialog", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog == null);
AddUntilStep("did perform", () => actionPerformed); AddUntilStep("did perform", () => actionPerformed);
} }
else else
@ -145,7 +146,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddWaitStep("wait a bit", 10); AddWaitStep("wait a bit", 10);
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded); waitForDialogOverlayLoad();
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == blocker2); AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == blocker2);
AddAssert("did not perform", () => !actionPerformed); AddAssert("did not perform", () => !actionPerformed);
@ -171,6 +172,48 @@ namespace osu.Game.Tests.Visual.Navigation
} }
} }
[TestCase(true)]
[TestCase(false)]
public void TestPerformBlockedByDialogSubScreen(bool confirm)
{
TestScreenWithNestedStack screenWithNestedStack = null;
PushAndConfirm(() => screenWithNestedStack = new TestScreenWithNestedStack());
AddAssert("wait for nested screen", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("wait for dialog", () => screenWithNestedStack.Blocker.ExitAttempts == 1);
AddWaitStep("wait a bit", 10);
waitForDialogOverlayLoad();
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == screenWithNestedStack);
AddAssert("nested screen didn't change", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
AddAssert("did not perform", () => !actionPerformed);
AddAssert("only one exit attempt", () => screenWithNestedStack.Blocker.ExitAttempts == 1);
if (confirm)
{
AddStep("accept dialog", () => InputManager.Key(Key.Number1));
AddAssert("nested screen changed", () => screenWithNestedStack.SubScreenStack.CurrentScreen != screenWithNestedStack.Blocker);
AddUntilStep("did perform", () => actionPerformed);
}
else
{
AddStep("cancel dialog", () => InputManager.Key(Key.Number2));
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == screenWithNestedStack);
AddAssert("nested screen didn't change", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
AddAssert("did not perform", () => !actionPerformed);
}
}
private void waitForDialogOverlayLoad() => AddUntilStep("wait for dialog overlay loaded", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
private void importAndWaitForSongSelect() private void importAndWaitForSongSelect()
{ {
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@ -181,13 +224,13 @@ namespace osu.Game.Tests.Visual.Navigation
public class DialogBlockingScreen : OsuScreen public class DialogBlockingScreen : OsuScreen
{ {
[Resolved] [Resolved]
private DialogOverlay dialogOverlay { get; set; } private IDialogOverlay dialogOverlay { get; set; }
private int dialogDisplayCount; private int dialogDisplayCount;
public int ExitAttempts { get; private set; } public int ExitAttempts { get; private set; }
public override bool OnExiting(IScreen next) public override bool OnExiting(ScreenExitEvent e)
{ {
ExitAttempts++; ExitAttempts++;
@ -197,7 +240,32 @@ namespace osu.Game.Tests.Visual.Navigation
return true; return true;
} }
return base.OnExiting(next); return base.OnExiting(e);
}
}
public class TestScreenWithNestedStack : OsuScreen, IHasSubScreenStack
{
public DialogBlockingScreen Blocker { get; private set; }
public ScreenStack SubScreenStack { get; } = new ScreenStack();
public TestScreenWithNestedStack()
{
AddInternal(SubScreenStack);
SubScreenStack.Push(Blocker = new DialogBlockingScreen());
}
public override bool OnExiting(ScreenExitEvent e)
{
if (SubScreenStack.CurrentScreen != null)
{
SubScreenStack.CurrentScreen.Exit();
return true;
}
return base.OnExiting(e);
} }
} }
} }

View File

@ -6,8 +6,8 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -23,12 +22,10 @@ using osu.Game.Scoring;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Skinning.Editor;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -70,73 +67,6 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
} }
[Test]
public void TestEditComponentDuringGameplay()
{
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
SkinEditor skinEditor = null;
AddStep("open skin editor", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.S);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType<SkinEditor>().FirstOrDefault()) != null);
AddStep("Click gameplay scene button", () =>
{
skinEditor.ChildrenOfType<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text == "Gameplay").TriggerClick();
});
AddUntilStep("wait for player", () =>
{
// dismiss any notifications that may appear (ie. muted notification).
clickMouseInCentre();
return Game.ScreenStack.CurrentScreen is Player;
});
BarHitErrorMeter hitErrorMeter = null;
AddUntilStep("select bar hit error blueprint", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().FirstOrDefault(b => b.Item is BarHitErrorMeter);
if (blueprint == null)
return false;
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
return true;
});
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
AddStep("hover first slider", () =>
{
InputManager.MoveMouseTo(
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
.ChildrenOfType<SettingsSlider<float>>().First()
.ChildrenOfType<SliderBar<float>>().First()
);
});
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
[Test] [Test]
public void TestRetryCountIncrements() public void TestRetryCountIncrements()
{ {
@ -154,8 +84,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () => AddUntilStep("wait for player", () =>
{ {
// dismiss any notifications that may appear (ie. muted notification). DismissAnyNotifications();
clickMouseInCentre();
return (player = Game.ScreenStack.CurrentScreen as Player) != null; return (player = Game.ScreenStack.CurrentScreen as Player) != null;
}); });
@ -200,10 +129,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("choose clear all scores", () => InputManager.Key(Key.Number4)); AddStep("choose clear all scores", () => InputManager.Key(Key.Number4));
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded); AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog != null); AddUntilStep("wait for dialog", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog == null);
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true)); AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
@ -246,10 +175,10 @@ namespace osu.Game.Tests.Visual.Navigation
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded); AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog != null); AddUntilStep("wait for dialog", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog == null);
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true)); AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
@ -279,8 +208,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () => AddUntilStep("wait for player", () =>
{ {
// dismiss any notifications that may appear (ie. muted notification). DismissAnyNotifications();
clickMouseInCentre();
return (player = Game.ScreenStack.CurrentScreen as Player) != null; return (player = Game.ScreenStack.CurrentScreen as Player) != null;
}); });
@ -595,8 +523,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () => AddUntilStep("wait for player", () =>
{ {
// dismiss any notifications that may appear (ie. muted notification). DismissAnyNotifications();
clickMouseInCentre();
return (player = Game.ScreenStack.CurrentScreen as Player) != null; return (player = Game.ScreenStack.CurrentScreen as Player) != null;
}); });
@ -606,12 +533,6 @@ namespace osu.Game.Tests.Visual.Navigation
return () => player; return () => player;
} }
private void clickMouseInCentre()
{
InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
}
private void pushEscape() => private void pushEscape() =>
AddStep("Press escape", () => InputManager.Key(Key.Escape)); AddStep("Press escape", () => InputManager.Key(Key.Escape));

View File

@ -0,0 +1,136 @@
// 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.Extensions;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning.Editor;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
using static osu.Game.Tests.Visual.Navigation.TestSceneScreenNavigation;
namespace osu.Game.Tests.Visual.Navigation
{
public class TestSceneSkinEditorSceneLibrary : OsuGameTestScene
{
private SkinEditor skinEditor;
public override void SetUpSteps()
{
base.SetUpSteps();
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("open skin editor", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.S);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType<SkinEditor>().FirstOrDefault()) != null);
}
[Test]
public void TestEditComponentDuringGameplay()
{
switchToGameplayScene();
BarHitErrorMeter hitErrorMeter = null;
AddUntilStep("select bar hit error blueprint", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().FirstOrDefault(b => b.Item is BarHitErrorMeter);
if (blueprint == null)
return false;
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
return true;
});
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
AddStep("hover first slider", () =>
{
InputManager.MoveMouseTo(
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
.ChildrenOfType<SettingsSlider<float>>().First()
.ChildrenOfType<SliderBar<float>>().First()
);
});
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
[Test]
public void TestAutoplayCompatibleModsRetainedOnEnteringGameplay()
{
AddStep("select DT", () => Game.SelectedMods.Value = new Mod[] { new OsuModDoubleTime() });
switchToGameplayScene();
AddAssert("DT still selected", () => ((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Single() is OsuModDoubleTime);
}
[Test]
public void TestAutoplayIncompatibleModsRemovedOnEnteringGameplay()
{
AddStep("select no fail and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModSpunOut() });
switchToGameplayScene();
AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
}
[Test]
public void TestDuplicateAutoplayModRemovedOnEnteringGameplay()
{
AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() });
switchToGameplayScene();
AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
}
[Test]
public void TestCinemaModRemovedOnEnteringGameplay()
{
AddStep("select cinema", () => Game.SelectedMods.Value = new Mod[] { new OsuModCinema() });
switchToGameplayScene();
AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
}
private void switchToGameplayScene()
{
AddStep("Click gameplay scene button", () => skinEditor.ChildrenOfType<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text == "Gameplay").TriggerClick());
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
return Game.ScreenStack.CurrentScreen is Player;
});
}
}
}

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