1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 00:47:24 +08:00

Merge branch 'master' into fl-skill

This commit is contained in:
smoogipoo 2021-09-10 16:39:39 +09:00
commit aafbc3e0e4
617 changed files with 14992 additions and 6481 deletions

163
.github/workflows/test-diffcalc.yml vendored Normal file
View File

@ -0,0 +1,163 @@
# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
# Usage:
# !pp check 0 | Runs only the osu! ruleset.
# !pp check 0 2 | Runs only the osu! and catch rulesets.
#
name: Diffcalc Consistency Checks
on:
issue_comment:
types: [ created ]
env:
DB_USER: root
DB_HOST: 127.0.0.1
CONCURRENCY: 4
ALLOW_DOWNLOAD: 1
SAVE_DOWNLOADED: 1
jobs:
diffcalc:
name: Diffcalc
runs-on: ubuntu-latest
continue-on-error: true
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '!pp check') &&
(github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
strategy:
fail-fast: false
matrix:
ruleset:
- { name: osu, id: 0 }
- { name: taiko, id: 1 }
- { name: catch, id: 2 }
- { name: mania, id: 3 }
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Verify ruleset
if: contains(github.event.comment.body, matrix.ruleset.id) == false
run: |
echo "${{ github.event.comment.body }} doesn't contain ${{ matrix.ruleset.id }}"
exit 1
- name: Verify MySQL connection from host
run: |
sudo apt-get install -y mysql-client
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "SHOW DATABASES"
- name: Create directory structure
run: |
mkdir -p $GITHUB_WORKSPACE/master/
mkdir -p $GITHUB_WORKSPACE/pr/
# Checkout osu
- name: Checkout osu (master)
uses: actions/checkout@v2
with:
repository: ppy/osu
path: 'master/osu'
- name: Checkout osu (pr)
uses: actions/checkout@v2
with:
path: 'pr/osu'
# Checkout osu-difficulty-calculator
- name: Checkout osu-difficulty-calculator (master)
uses: actions/checkout@v2
with:
repository: ppy/osu-difficulty-calculator
path: 'master/osu-difficulty-calculator'
- name: Checkout osu-difficulty-calculator (pr)
uses: actions/checkout@v2
with:
repository: ppy/osu-difficulty-calculator
path: 'pr/osu-difficulty-calculator'
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
# Sanity checks to make sure diffcalc is not run when incompatible.
- name: Build diffcalc (master)
run: |
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator
./UseLocalOsu.sh
dotnet build
- name: Build diffcalc (pr)
run: |
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator
./UseLocalOsu.sh
dotnet build
# Initial data imports
- name: Download + import data
run: |
PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
# Set env variable for further steps.
echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV
cd $GITHUB_WORKSPACE
wget https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2
wget https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2
tar -xf $PERFORMANCE_DATA_NAME.tar.bz2
tar -xf $BEATMAPS_DATA_NAME.tar.bz2
cd $GITHUB_WORKSPACE/$PERFORMANCE_DATA_NAME
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_master"
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_pr"
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_master
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_pr
# Run diffcalc
- name: Run diffcalc (master)
env:
DB_NAME: osu_master
run: |
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
- name: Run diffcalc (pr)
env:
DB_NAME: osu_pr
run: |
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
# Print diffs
- name: Print diffs
run: |
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "
SELECT
m.beatmap_id,
m.mods,
m.diff_unified as 'sr_master',
p.diff_unified as 'sr_pr',
(p.diff_unified - m.diff_unified) as 'diff'
FROM osu_master.osu_beatmap_difficulty m
JOIN osu_pr.osu_beatmap_difficulty p
ON m.beatmap_id = p.beatmap_id
AND m.mode = p.mode
AND m.mods = p.mods
WHERE abs(m.diff_unified - p.diff_unified) > 0.1
ORDER BY abs(m.diff_unified - p.diff_unified)
DESC
LIMIT 10000;"
# Todo: Run ppcalc

View File

@ -1,8 +1,8 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Benchmarks" type="DotNetProject" factoryName=".NET Project"> <configuration default="false" name="Benchmarks" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Release/net5.0/osu.Game.Benchmarks.dll" /> <option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Debug/net5.0/osu.Game.Benchmarks.dll" />
<option name="PROGRAM_PARAMETERS" value="--filter *" /> <option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Release/net5.0" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Debug/net5.0" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
@ -14,7 +14,7 @@
<option name="PROJECT_KIND" value="DotNetCore" /> <option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net5.0" /> <option name="PROJECT_TFM" value="net5.0" />
<method v="2"> <method v="2">
<option name="Build" enabled="true" /> <option name="Build" />
</method> </method>
</configuration> </configuration>
</component> </component>

View File

@ -4,7 +4,7 @@
# osu! # osu!
[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![Build status](https://github.com/ppy/osu/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/ppy/osu/actions/workflows/ci.yml)
[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest) [![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest)
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)
@ -31,12 +31,11 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:** **Latest build:**
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) | [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- | | ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets. - The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
- When running on Windows 7 or 8.1, *[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net50&pivots=os-windows#dependencies)** may be required to correctly run .NET 5 applications if your operating system is not up-to-date with the latest service packs.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
## Developing a custom ruleset ## Developing a custom ruleset

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

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="2021.808.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.807.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.907.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

@ -20,8 +20,21 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)] [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-archive")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed", "application/x-osu-archive" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[]
{
"application/zip",
"application/octet-stream",
"application/download",
"application/x-zip",
"application/x-zip-compressed",
// newer official mime types (see https://osu.ppy.sh/wiki/en/osu%21_File_Formats).
"application/x-osu-beatmap-archive",
"application/x-osu-skin-archive",
"application/x-osu-replay",
})]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity public class OsuGameActivity : AndroidGameActivity
{ {
@ -66,12 +79,14 @@ namespace osu.Android
case Intent.ActionSendMultiple: case Intent.ActionSendMultiple:
{ {
var uris = new List<Uri>(); var uris = new List<Uri>();
for (int i = 0; i < intent.ClipData?.ItemCount; i++) for (int i = 0; i < intent.ClipData?.ItemCount; i++)
{ {
var content = intent.ClipData?.GetItemAt(i); var content = intent.ClipData?.GetItemAt(i);
if (content != null) if (content != null)
uris.Add(content.Uri); uris.Add(content.Uri);
} }
handleImportFromUris(uris.ToArray()); handleImportFromUris(uris.ToArray());
break; break;
} }

View File

@ -139,8 +139,8 @@ namespace osu.Desktop
{ {
switch (activity) switch (activity)
{ {
case UserActivity.SoloGame solo: case UserActivity.InGame game:
return solo.Beatmap.ToString(); return game.Beatmap.ToString();
case UserActivity.Editing edit: case UserActivity.Editing edit:
return edit.Beatmap.ToString(); return edit.Beatmap.ToString();

View File

@ -5,23 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Screens.Play;
namespace osu.Desktop.Windows namespace osu.Desktop.Windows
{ {
public class GameplayWinKeyBlocker : Component public class GameplayWinKeyBlocker : Component
{ {
private Bindable<bool> disableWinKey; private Bindable<bool> disableWinKey;
private Bindable<bool> localUserPlaying; private IBindable<bool> localUserPlaying;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuGame game, OsuConfigManager config) private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)
{ {
localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking()); localUserPlaying.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey); disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);

View File

@ -0,0 +1,34 @@
// 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 BenchmarkDotNet.Attributes;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Benchmarks
{
public class BenchmarkMod : BenchmarkTest
{
private OsuModDoubleTime mod;
[Params(1, 10, 100)]
public int Times { get; set; }
public override void SetUp()
{
base.SetUp();
mod = new OsuModDoubleTime();
}
[Benchmark]
public int ModHashCode()
{
var hashCode = new HashCode();
for (int i = 0; i < Times; i++)
hashCode.Add(mod);
return hashCode.ToHashCode();
}
}
}

View File

@ -0,0 +1,62 @@
// 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 BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using osu.Game.Online.API;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Benchmarks
{
public class BenchmarkRuleset : BenchmarkTest
{
private OsuRuleset ruleset;
private APIMod apiModDoubleTime;
private APIMod apiModDifficultyAdjust;
public override void SetUp()
{
base.SetUp();
ruleset = new OsuRuleset();
apiModDoubleTime = new APIMod { Acronym = "DT" };
apiModDifficultyAdjust = new APIMod { Acronym = "DA" };
}
[Benchmark]
public void BenchmarkToModDoubleTime()
{
apiModDoubleTime.ToMod(ruleset);
}
[Benchmark]
public void BenchmarkToModDifficultyAdjust()
{
apiModDifficultyAdjust.ToMod(ruleset);
}
[Benchmark]
public void BenchmarkGetAllMods()
{
ruleset.CreateAllMods().Consume(new Consumer());
}
[Benchmark]
public void BenchmarkGetAllModsForReference()
{
ruleset.AllMods.Consume(new Consumer());
}
[Benchmark]
public void BenchmarkGetForAcronym()
{
ruleset.CreateModFromAcronym("DT");
}
[Benchmark]
public void BenchmarkGetForType()
{
ruleset.CreateMod<ModDoubleTime>();
}
}
}

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 BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running; using BenchmarkDotNet.Running;
namespace osu.Game.Benchmarks namespace osu.Game.Benchmarks
@ -11,7 +12,7 @@ namespace osu.Game.Benchmarks
{ {
BenchmarkSwitcher BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly) .FromAssembly(typeof(Program).Assembly)
.Run(args); .Run(args, DefaultConfig.Instance.WithOption(ConfigOptions.DisableOptimizationsValidator, true));
} }
} }
} }

View File

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" /> <PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="nunit" Version="3.13.2" /> <PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
</ItemGroup> </ItemGroup>

View File

@ -210,9 +210,9 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
new Vector2(50, 200), new Vector2(50, 200),
}), 0.5); }), 0.5);
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count); AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.PerfectCurve); AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PerfectCurve);
addAddVertexSteps(150, 150); addAddVertexSteps(150, 150);
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type.Value == PathType.Linear); AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.Linear);
} }
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () => private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>

View File

@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Catch.Tests
} while (rng.Next(2) != 0); } while (rng.Next(2) != 0);
int length = sliderPath.ControlPoints.Count - start + 1; int length = sliderPath.ControlPoints.Count - start + 1;
sliderPath.ControlPoints[start].Type.Value = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier; sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
} while (rng.Next(3) != 0); } while (rng.Next(3) != 0);
if (rng.Next(5) == 0) if (rng.Next(5) == 0)
@ -210,13 +210,13 @@ namespace osu.Game.Rulesets.Catch.Tests
path.ConvertToSliderPath(sliderPath, sliderStartY); path.ConvertToSliderPath(sliderPath, sliderStartY);
Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3)); Assert.That(sliderPath.Distance, Is.EqualTo(path.Distance).Within(1e-3));
Assert.That(sliderPath.ControlPoints[0].Position.Value.X, Is.EqualTo(path.Vertices[0].X)); Assert.That(sliderPath.ControlPoints[0].Position.X, Is.EqualTo(path.Vertices[0].X));
assertInvariants(path.Vertices, true); assertInvariants(path.Vertices, true);
foreach (var point in sliderPath.ControlPoints) foreach (var point in sliderPath.ControlPoints)
{ {
Assert.That(point.Type.Value, Is.EqualTo(PathType.Linear).Or.Null); Assert.That(point.Type, Is.EqualTo(PathType.Linear).Or.Null);
Assert.That(sliderStartY + point.Position.Value.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT)); Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
} }
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)

View File

@ -0,0 +1,120 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
[TestFixture]
public class CatchModMirrorTest
{
[Test]
public void TestModMirror()
{
IBeatmap original = createBeatmap(false);
IBeatmap mirrored = createBeatmap(true);
assertEffectivePositionsMirrored(original, mirrored);
}
private static IBeatmap createBeatmap(bool withMirrorMod)
{
var beatmap = createRawBeatmap();
var mirrorMod = new CatchModMirror();
var beatmapProcessor = new CatchBeatmapProcessor(beatmap);
beatmapProcessor.PreProcess();
foreach (var hitObject in beatmap.HitObjects)
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
beatmapProcessor.PostProcess();
if (withMirrorMod)
mirrorMod.ApplyToBeatmap(beatmap);
return beatmap;
}
private static IBeatmap createRawBeatmap() => new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
OriginalX = 150,
StartTime = 0
},
new Fruit
{
OriginalX = 450,
StartTime = 500
},
new JuiceStream
{
OriginalX = 250,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(new Vector2(-100, 1)),
new PathControlPoint(new Vector2(0, 2)),
new PathControlPoint(new Vector2(100, 3)),
new PathControlPoint(new Vector2(0, 4))
}
},
StartTime = 1000,
},
new BananaShower
{
StartTime = 5000,
Duration = 5000
}
}
};
private static void assertEffectivePositionsMirrored(IBeatmap original, IBeatmap mirrored)
{
if (original.HitObjects.Count != mirrored.HitObjects.Count)
Assert.Fail($"Top-level object count mismatch (original: {original.HitObjects.Count}, mirrored: {mirrored.HitObjects.Count})");
for (int i = 0; i < original.HitObjects.Count; ++i)
{
var originalObject = (CatchHitObject)original.HitObjects[i];
var mirroredObject = (CatchHitObject)mirrored.HitObjects[i];
// banana showers themselves are exempt, as we only really care about their nested bananas' positions.
if (!effectivePositionMirrored(originalObject, mirroredObject) && !(originalObject is BananaShower))
Assert.Fail($"{originalObject.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalObject, mirroredObject)})");
if (originalObject.NestedHitObjects.Count != mirroredObject.NestedHitObjects.Count)
Assert.Fail($"{originalObject.GetType().Name} nested object count mismatch (original: {originalObject.NestedHitObjects.Count}, mirrored: {mirroredObject.NestedHitObjects.Count})");
for (int j = 0; j < originalObject.NestedHitObjects.Count; ++j)
{
var originalNested = (CatchHitObject)originalObject.NestedHitObjects[j];
var mirroredNested = (CatchHitObject)mirroredObject.NestedHitObjects[j];
if (!effectivePositionMirrored(originalNested, mirroredNested))
Assert.Fail($"{originalObject.GetType().Name}'s nested {originalNested.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalNested, mirroredNested)})");
}
}
}
private static string printEffectivePositions(CatchHitObject original, CatchHitObject mirrored)
=> $"original X: {original.EffectiveX}, mirrored X is: {mirrored.EffectiveX}, mirrored X should be: {CatchPlayfield.WIDTH - original.EffectiveX}";
private static bool effectivePositionMirrored(CatchHitObject original, CatchHitObject mirrored)
=> Precision.AlmostEquals(original.EffectiveX, CatchPlayfield.WIDTH - mirrored.EffectiveX);
}
}

View File

@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
AddSliderStep<float>("circle size", 0, 8, 5, createCatcher); AddSliderStep<float>("circle size", 0, 8, 5, createCatcher);
AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t))); AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t)));
AddToggleStep("toggle hit lighting", lighting => config.SetValue(OsuSetting.HitLighting, lighting));
AddStep("catch centered fruit", () => attemptCatch(new Fruit())); AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
AddStep("catch many random fruit", () => AddStep("catch many random fruit", () =>

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case JuiceStream juiceStream: case JuiceStream juiceStream:
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead. // Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.Value.X; lastPosition = juiceStream.OriginalX + juiceStream.Path.ControlPoints[^1].Position.X;
// Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead. // Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
lastStartTime = juiceStream.StartTime; lastStartTime = juiceStream.StartTime;

View File

@ -117,6 +117,7 @@ namespace osu.Game.Rulesets.Catch
{ {
new CatchModDifficultyAdjust(), new CatchModDifficultyAdjust(),
new CatchModClassic(), new CatchModClassic(),
new CatchModMirror(),
}; };
case ModType.Automation: case ModType.Automation:

View File

@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Catch
Banana, Banana,
Droplet, Droplet,
Catcher, Catcher,
CatchComboCounter CatchComboCounter,
HitExplosion
} }
} }

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{ {
public class Movement : StrainSkill public class Movement : StrainDecaySkill
{ {
private const float absolute_player_positioning_error = 16f; private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f; private const float normalized_hitobject_radius = 41.0f;

View File

@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
path.ConvertFromSliderPath(sliderPath); path.ConvertFromSliderPath(sliderPath);
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices. // If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
if (sliderPath.ControlPoints.Any(p => p.Type.Value != null && p.Type.Value != PathType.Linear)) if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
{ {
path.ResampleVertices(hitObject.NestedHitObjects path.ResampleVertices(hitObject.NestedHitObjects
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used. .Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.

View File

@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Catch.Edit
juiceStream.OriginalX = selectionRange.GetFlippedPosition(juiceStream.OriginalX); juiceStream.OriginalX = selectionRange.GetFlippedPosition(juiceStream.OriginalX);
foreach (var point in juiceStream.Path.ControlPoints) foreach (var point in juiceStream.Path.ControlPoints)
point.Position.Value *= new Vector2(-1, 1); point.Position *= new Vector2(-1, 1);
EditorBeatmap.Update(juiceStream); EditorBeatmap.Update(juiceStream);
return true; return true;

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModMirror : ModMirror, IApplicableToBeatmap
{
public override string Description => "Fruits are flipped horizontally.";
/// <remarks>
/// <see cref="IApplicableToBeatmap"/> is used instead of <see cref="IApplicableToHitObject"/>,
/// as <see cref="CatchBeatmapProcessor"/> applies offsets in <see cref="CatchBeatmapProcessor.PostProcess"/>.
/// <see cref="IApplicableToBeatmap"/> runs after post-processing, while <see cref="IApplicableToHitObject"/> runs before it.
/// </remarks>
public void ApplyToBeatmap(IBeatmap beatmap)
{
foreach (var hitObject in beatmap.HitObjects)
applyToHitObject(hitObject);
}
private void applyToHitObject(HitObject hitObject)
{
var catchObject = (CatchHitObject)hitObject;
switch (catchObject)
{
case Fruit fruit:
mirrorEffectiveX(fruit);
break;
case JuiceStream juiceStream:
mirrorEffectiveX(juiceStream);
mirrorJuiceStreamPath(juiceStream);
break;
case BananaShower bananaShower:
mirrorBananaShower(bananaShower);
break;
}
}
/// <summary>
/// Mirrors the effective X position of <paramref name="catchObject"/> and its nested hit objects.
/// </summary>
private static void mirrorEffectiveX(CatchHitObject catchObject)
{
catchObject.OriginalX = CatchPlayfield.WIDTH - catchObject.OriginalX;
catchObject.XOffset = -catchObject.XOffset;
foreach (var nested in catchObject.NestedHitObjects.Cast<CatchHitObject>())
{
nested.OriginalX = CatchPlayfield.WIDTH - nested.OriginalX;
nested.XOffset = -nested.XOffset;
}
}
/// <summary>
/// Mirrors the path of the <paramref name="juiceStream"/>.
/// </summary>
private static void mirrorJuiceStreamPath(JuiceStream juiceStream)
{
var controlPoints = juiceStream.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints)
point.Position = new Vector2(-point.Position.X, point.Position.Y);
juiceStream.Path = new SliderPath(controlPoints, juiceStream.Path.ExpectedDistance.Value);
}
/// <summary>
/// Mirrors X positions of all bananas in the <paramref name="bananaShower"/>.
/// </summary>
private static void mirrorBananaShower(BananaShower bananaShower)
{
foreach (var banana in bananaShower.NestedHitObjects.OfType<Banana>())
banana.XOffset = CatchPlayfield.WIDTH - banana.XOffset;
}
}
}

View File

@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Catch.Objects
if (value != null) if (value != null)
{ {
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position.Value, c.Type.Value))); path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
path.ExpectedDistance.Value = value.ExpectedDistance.Value; path.ExpectedDistance.Value = value.ExpectedDistance.Value;
} }
} }

View File

@ -234,7 +234,7 @@ namespace osu.Game.Rulesets.Catch.Objects
for (int i = 1; i < vertices.Count; i++) for (int i = 1; i < vertices.Count; i++)
{ {
sliderPath.ControlPoints[^1].Type.Value = PathType.Linear; sliderPath.ControlPoints[^1].Type = PathType.Linear;
float deltaX = vertices[i].X - lastPosition.X; float deltaX = vertices[i].X - lastPosition.X;
double length = vertices[i].Distance - currentDistance; double length = vertices[i].Distance - currentDistance;

View File

@ -0,0 +1,129 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class DefaultHitExplosion : CompositeDrawable, IHitExplosion
{
private CircularContainer largeFaint;
private CircularContainer smallFaint;
private CircularContainer directionalGlow1;
private CircularContainer directionalGlow2;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(20);
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
// scale roughly in-line with visual appearance of notes
const float initial_height = 10;
InternalChildren = new Drawable[]
{
largeFaint = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
},
smallFaint = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
},
directionalGlow1 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
},
directionalGlow2 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
}
};
}
public void Animate(HitExplosionEntry entry)
{
X = entry.Position;
Scale = new Vector2(entry.HitObject.Scale);
setColour(entry.ObjectColour);
using (BeginAbsoluteSequence(entry.LifetimeStart))
applyTransforms(entry.HitObject.RandomSeed);
}
private void applyTransforms(int randomSeed)
{
const double duration = 400;
// we want our size to be very small so the glow dominates it.
largeFaint.Size = new Vector2(0.8f);
largeFaint
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
.FadeOut(duration * 2);
const float angle_variangle = 15; // should be less than 45
directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
}
private void setColour(Color4 objectColour)
{
const float roundness = 100;
largeFaint.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
Roundness = 160,
Radius = 200,
};
smallFaint.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
Roundness = 20,
Radius = 50,
};
directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
Roundness = roundness,
Radius = 40,
};
}
}
}

View File

@ -70,13 +70,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
if (version < 2.3m) if (version < 2.3m)
{ {
if (GetTexture(@"fruit-ryuuta") != null || if (hasOldStyleCatcherSprite())
GetTexture(@"fruit-ryuuta-0") != null)
return new LegacyCatcherOld(); return new LegacyCatcherOld();
} }
if (GetTexture(@"fruit-catcher-idle") != null || if (hasNewStyleCatcherSprite())
GetTexture(@"fruit-catcher-idle-0") != null)
return new LegacyCatcherNew(); return new LegacyCatcherNew();
return null; return null;
@ -86,12 +84,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new LegacyCatchComboCounter(Skin); return new LegacyCatchComboCounter(Skin);
return null; return null;
case CatchSkinComponents.HitExplosion:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
return new LegacyHitExplosion();
return null;
} }
} }
return base.GetDrawableComponent(component); return base.GetDrawableComponent(component);
} }
private bool hasOldStyleCatcherSprite() =>
GetTexture(@"fruit-ryuuta") != null
|| GetTexture(@"fruit-ryuuta-0") != null;
private bool hasNewStyleCatcherSprite() =>
GetTexture(@"fruit-catcher-idle") != null
|| GetTexture(@"fruit-catcher-idle-0") != null;
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)

View File

@ -0,0 +1,94 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public class LegacyHitExplosion : CompositeDrawable, IHitExplosion
{
[Resolved]
private Catcher catcher { get; set; }
private const float catch_margin = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2;
private readonly Sprite explosion1;
private readonly Sprite explosion2;
public LegacyHitExplosion()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
RelativeSizeAxes = Axes.Both;
Scale = new Vector2(0.5f);
InternalChildren = new[]
{
explosion1 = new Sprite
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Alpha = 0,
Blending = BlendingParameters.Additive,
Rotation = -90
},
explosion2 = new Sprite
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Alpha = 0,
Blending = BlendingParameters.Additive,
Rotation = -90
}
};
}
[BackgroundDependencyLoader]
private void load(SkinManager skins)
{
var defaultLegacySkin = skins.DefaultLegacySkin;
// sprite names intentionally swapped to match stable member naming / ease of cross-referencing
explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2");
explosion2.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-1");
}
public void Animate(HitExplosionEntry entry)
{
Colour = entry.ObjectColour;
using (BeginAbsoluteSequence(entry.LifetimeStart))
{
float halfCatchWidth = catcher.CatchWidth / 2;
float explosionOffset = Math.Clamp(entry.Position, -halfCatchWidth + catch_margin * 3, halfCatchWidth - catch_margin * 3);
if (!(entry.HitObject is Droplet))
{
float scale = Math.Clamp(entry.JudgementResult.ComboAtJudgement / 200f, 0.35f, 1.125f);
explosion1.Scale = new Vector2(1, 0.9f);
explosion1.Position = new Vector2(explosionOffset, 0);
explosion1.FadeOutFromOne(300);
explosion1.ScaleTo(new Vector2(16 * scale, 1.1f), 160, Easing.Out);
}
explosion2.Scale = new Vector2(0.9f, 1);
explosion2.Position = new Vector2(explosionOffset, 0);
explosion2.FadeOutFromOne(700);
explosion2.ScaleTo(new Vector2(0.9f, 1.3f), 500, Easing.Out);
this.Delay(700).FadeOutFromOne();
}
}
}
}

View File

@ -23,6 +23,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
[Cached]
public class Catcher : SkinReloadableDrawable public class Catcher : SkinReloadableDrawable
{ {
/// <summary> /// <summary>
@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary> /// <summary>
/// Width of the area that can be used to attempt catches during gameplay. /// Width of the area that can be used to attempt catches during gameplay.
/// </summary> /// </summary>
private readonly float catchWidth; public readonly float CatchWidth;
private readonly SkinnableCatcher body; private readonly SkinnableCatcher body;
@ -133,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (difficulty != null) if (difficulty != null)
Scale = calculateScale(difficulty); Scale = calculateScale(difficulty);
catchWidth = CalculateCatchWidth(Scale); CatchWidth = CalculateCatchWidth(Scale);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (!(hitObject is PalpableCatchHitObject fruit)) if (!(hitObject is PalpableCatchHitObject fruit))
return false; return false;
float halfCatchWidth = catchWidth * 0.5f; float halfCatchWidth = CatchWidth * 0.5f;
return fruit.EffectiveX >= X - halfCatchWidth && return fruit.EffectiveX >= X - halfCatchWidth &&
fruit.EffectiveX <= X + halfCatchWidth; fruit.EffectiveX <= X + halfCatchWidth;
} }
@ -216,7 +217,7 @@ namespace osu.Game.Rulesets.Catch.UI
placeCaughtObject(palpableObject, positionInStack); placeCaughtObject(palpableObject, positionInStack);
if (hitLighting.Value) if (hitLighting.Value)
addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value); addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
} }
// droplet doesn't affect the catcher state // droplet doesn't affect the catcher state
@ -365,8 +366,8 @@ namespace osu.Game.Rulesets.Catch.UI
return position; return position;
} }
private void addLighting(CatchHitObject hitObject, float x, Color4 colour) => private void addLighting(JudgementResult judgementResult, Color4 colour, float x) =>
hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed)); hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, judgementResult, colour, x));
private CaughtObject getCaughtObject(PalpableCatchHitObject source) private CaughtObject getCaughtObject(PalpableCatchHitObject source)
{ {

View File

@ -1,129 +1,56 @@
// 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 osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Pooling;
using osu.Game.Utils; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; #nullable enable
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
public class HitExplosion : PoolableDrawableWithLifetime<HitExplosionEntry> public class HitExplosion : PoolableDrawableWithLifetime<HitExplosionEntry>
{ {
private readonly CircularContainer largeFaint; private readonly SkinnableDrawable skinnableExplosion;
private readonly CircularContainer smallFaint;
private readonly CircularContainer directionalGlow1;
private readonly CircularContainer directionalGlow2;
public HitExplosion() public HitExplosion()
{ {
Size = new Vector2(20); RelativeSizeAxes = Axes.Both;
Anchor = Anchor.TopCentre; Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre; Origin = Anchor.BottomCentre;
// scale roughly in-line with visual appearance of notes InternalChild = skinnableExplosion = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.HitExplosion), _ => new DefaultHitExplosion())
const float initial_height = 10;
InternalChildren = new Drawable[]
{ {
largeFaint = new CircularContainer CentreComponent = false,
{ Anchor = Anchor.BottomCentre,
Anchor = Anchor.Centre, Origin = Anchor.BottomCentre
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
},
smallFaint = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
},
directionalGlow1 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
},
directionalGlow2 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
}
}; };
} }
protected override void OnApply(HitExplosionEntry entry) protected override void OnApply(HitExplosionEntry entry)
{ {
X = entry.Position; base.OnApply(entry);
Scale = new Vector2(entry.Scale); if (IsLoaded)
setColour(entry.ObjectColour); apply(entry);
using (BeginAbsoluteSequence(entry.LifetimeStart))
applyTransforms(entry.RNGSeed);
} }
private void applyTransforms(int randomSeed) protected override void LoadComplete()
{ {
base.LoadComplete();
apply(Entry);
}
private void apply(HitExplosionEntry? entry)
{
if (entry == null)
return;
ApplyTransformsAt(double.MinValue, true);
ClearTransforms(true); ClearTransforms(true);
const double duration = 400; (skinnableExplosion.Drawable as IHitExplosion)?.Animate(entry);
LifetimeEnd = skinnableExplosion.Drawable.LatestTransformEndTime;
// we want our size to be very small so the glow dominates it.
largeFaint.Size = new Vector2(0.8f);
largeFaint
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
.FadeOut(duration * 2);
const float angle_variangle = 15; // should be less than 45
directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire();
}
private void setColour(Color4 objectColour)
{
const float roundness = 100;
largeFaint.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
Roundness = 160,
Radius = 200,
};
smallFaint.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
Roundness = 20,
Radius = 50,
};
directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
Roundness = roundness,
Radius = 40,
};
} }
} }
} }

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 osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Pooling;
@ -14,6 +15,8 @@ namespace osu.Game.Rulesets.Catch.UI
public HitExplosionContainer() public HitExplosionContainer()
{ {
RelativeSizeAxes = Axes.Both;
AddInternal(pool = new DrawablePool<HitExplosion>(10)); AddInternal(pool = new DrawablePool<HitExplosion>(10));
} }

View File

@ -2,24 +2,42 @@
// 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 osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osuTK.Graphics; using osuTK.Graphics;
#nullable enable
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
public class HitExplosionEntry : LifetimeEntry public class HitExplosionEntry : LifetimeEntry
{ {
public readonly float Position; /// <summary>
public readonly float Scale; /// The judgement result that triggered this explosion.
public readonly Color4 ObjectColour; /// </summary>
public readonly int RNGSeed; public JudgementResult JudgementResult { get; }
public HitExplosionEntry(double startTime, float position, float scale, Color4 objectColour, int rngSeed) /// <summary>
/// The hitobject which triggered this explosion.
/// </summary>
public CatchHitObject HitObject => (CatchHitObject)JudgementResult.HitObject;
/// <summary>
/// The accent colour of the object caught.
/// </summary>
public Color4 ObjectColour { get; }
/// <summary>
/// The position at which the object was caught.
/// </summary>
public float Position { get; }
public HitExplosionEntry(double startTime, JudgementResult judgementResult, Color4 objectColour, float position)
{ {
LifetimeStart = startTime; LifetimeStart = startTime;
Position = position; Position = position;
Scale = scale; JudgementResult = judgementResult;
ObjectColour = objectColour; ObjectColour = objectColour;
RNGSeed = rngSeed;
} }
} }
} }

View File

@ -0,0 +1,18 @@
// 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
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// Common interface for all hit explosion skinnables.
/// </summary>
public interface IHitExplosion
{
/// <summary>
/// Begins animating this <see cref="IHitExplosion"/>.
/// </summary>
void Animate(HitExplosionEntry entry);
}
}

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public class TestSceneManiaComposeScreen : EditorClockTestScene
{
[Resolved]
private SkinManager skins { get; set; }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup compose screen", () =>
{
var editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }))
{
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
};
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);
}
[Test]
public void TestDefaultSkin()
{
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = SkinInfo.Default);
}
[Test]
public void TestLegacySkin()
{
AddStep("set legacy skin", () => skins.CurrentSkinInfo.Value = DefaultLegacySkin.Info);
}
}
}

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{ {
public class Strain : StrainSkill public class Strain : StrainDecaySkill
{ {
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;
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
return individualStrain + overallStrain - CurrentStrain; return individualStrain + overallStrain - CurrentStrain;
} }
protected override double GetPeakStrain(double offset) protected override double CalculateInitialStrain(double offset)
=> applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base) => applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base)
+ applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base); + applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base);

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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Mania.Edit.Setup
{
public class ManiaSetupSection : RulesetSetupSection
{
private LabelledSwitchButton specialStyle;
public ManiaSetupSection()
: base(new ManiaRuleset().RulesetInfo)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
specialStyle = new LabelledSwitchButton
{
Label = "Use special (N+1) style",
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 5k (4+1) or 8key (7+1) configurations.",
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
specialStyle.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateBeatmap()
{
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
}
}
}

View File

@ -27,11 +27,13 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Difficulty; using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Edit.Setup;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mania.Skinning.Legacy; using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania namespace osu.Game.Rulesets.Mania
@ -390,6 +392,8 @@ namespace osu.Game.Rulesets.Mania
{ {
return new ManiaFilterCriteria(); return new ManiaFilterCriteria();
} }
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
} }
public enum PlayfieldType public enum PlayfieldType

View File

@ -275,9 +275,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
return false; return false;
beginHoldAt(Time.Current - Head.HitObject.StartTime); beginHoldAt(Time.Current - Head.HitObject.StartTime);
Head.UpdateResult();
return true; return Head.UpdateResult();
} }
private void beginHoldAt(double timeOffset) private void beginHoldAt(double timeOffset)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
} }
public void UpdateResult() => base.UpdateResult(true); public bool UpdateResult() => base.UpdateResult(true);
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()
{ {

View File

@ -6,7 +6,6 @@ using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
@ -29,11 +28,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; } private ManiaPlayfield playfield { get; set; }
/// <summary>
/// Gets the samples that are played by this object during gameplay.
/// </summary>
public ISampleInfo[] GetGameplaySamples() => Samples.Samples;
protected override float SamplePlaybackPosition protected override float SamplePlaybackPosition
{ {
get get

View File

@ -1,7 +1,6 @@
// 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.Linq;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -19,6 +18,7 @@ using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
@ -28,12 +28,6 @@ namespace osu.Game.Rulesets.Mania.UI
public const float COLUMN_WIDTH = 80; public const float COLUMN_WIDTH = 80;
public const float SPECIAL_COLUMN_WIDTH = 70; public const float SPECIAL_COLUMN_WIDTH = 70;
/// <summary>
/// For hitsounds played by this <see cref="Column"/> (i.e. not as a result of hitting a hitobject),
/// a certain number of samples are allowed to be played concurrently so that it feels better when spam-pressing the key.
/// </summary>
private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY;
/// <summary> /// <summary>
/// The index of this column as part of the whole playfield. /// The index of this column as part of the whole playfield.
/// </summary> /// </summary>
@ -45,10 +39,10 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer; internal readonly Container TopLevelContainer;
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool; private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy; private readonly OrderedHitPolicy hitPolicy;
private readonly Container<SkinnableSound> hitSounds;
public Container UnderlayElements => HitObjectArea.UnderlayElements; public Container UnderlayElements => HitObjectArea.UnderlayElements;
private readonly GameplaySampleTriggerSource sampleTriggerSource;
public Column(int index) public Column(int index)
{ {
Index = index; Index = index;
@ -64,6 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new[] InternalChildren = new[]
{ {
hitExplosionPool = new DrawablePool<PoolableHitExplosion>(5), hitExplosionPool = new DrawablePool<PoolableHitExplosion>(5),
sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(), background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
@ -72,12 +67,6 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
background, background,
hitSounds = new Container<SkinnableSound>
{
Name = "Column samples pool",
RelativeSizeAxes = Axes.Both,
Children = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray()
},
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
}; };
@ -133,29 +122,12 @@ namespace osu.Game.Rulesets.Mania.UI
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
} }
private int nextHitSoundIndex;
public bool OnPressed(ManiaAction action) public bool OnPressed(ManiaAction action)
{ {
if (action != Action.Value) if (action != Action.Value)
return false; return false;
var nextObject = sampleTriggerSource.Play();
HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
// fallback to non-alive objects to find next off-screen object
HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
HitObjectContainer.Objects.LastOrDefault();
if (nextObject is DrawableManiaHitObject maniaObject)
{
var hitSound = hitSounds[nextHitSoundIndex];
hitSound.Samples = maniaObject.GetGameplaySamples();
hitSound.Play();
nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds;
}
return true; return true;
} }

View File

@ -37,12 +37,11 @@ namespace osu.Game.Rulesets.Mania.UI
public override void PlayAnimation() public override void PlayAnimation()
{ {
base.PlayAnimation();
switch (Result) switch (Result)
{ {
case HitResult.None: case HitResult.None:
case HitResult.Miss: case HitResult.Miss:
base.PlayAnimation();
break; break;
default: default:
@ -52,6 +51,8 @@ namespace osu.Game.Rulesets.Mania.UI
this.Delay(50) this.Delay(50)
.ScaleTo(0.75f, 250) .ScaleTo(0.75f, 250)
.FadeOut(200); .FadeOut(200);
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
break; break;
} }
} }

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("last connection displayed", () => AddAssert("last connection displayed", () =>
{ {
var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position.Value == new Vector2(300)); var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position == new Vector2(300));
return lastConnection.DrawWidth > 50; return lastConnection.DrawWidth > 50;
}); });
} }
@ -166,14 +166,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
AddStep($"move mouse to control point {index}", () => AddStep($"move mouse to control point {index}", () =>
{ {
Vector2 position = slider.Path.ControlPoints[index].Position.Value; Vector2 position = slider.Path.ControlPoints[index].Position;
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position)); InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
}); });
} }
private void assertControlPointPathType(int controlPointIndex, PathType? type) private void assertControlPointPathType(int controlPointIndex, PathType? type)
{ {
AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type.Value == type); AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type == type);
} }
private void addContextMenuItemStep(string contextMenuText) private void addContextMenuItemStep(string contextMenuText)

View File

@ -108,9 +108,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test] [Test]
public void TestDragControlPointPathAfterChangingType() public void TestDragControlPointPathAfterChangingType()
{ {
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type.Value = PathType.Bezier); AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type = PathType.Bezier);
AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10)))); AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10))));
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type.Value = PathType.PerfectCurve); AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type = PathType.PerfectCurve);
moveMouseToControlPoint(4); moveMouseToControlPoint(4);
AddStep("hold", () => InputManager.PressButton(MouseButton.Left)); AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
@ -137,15 +137,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
AddStep($"move mouse to control point {index}", () => AddStep($"move mouse to control point {index}", () =>
{ {
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value; Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
}); });
} }
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type.Value == type); private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type == type);
private void assertControlPointPosition(int index, Vector2 position) => private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position.Value, 1)); AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position, 1));
private class TestSliderBlueprint : SliderSelectionBlueprint private class TestSliderBlueprint : SliderSelectionBlueprint
{ {

View File

@ -385,10 +385,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected); private void assertControlPointCount(int expected) => AddAssert($"has {expected} control points", () => getSlider().Path.ControlPoints.Count == expected);
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type.Value == type); private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => getSlider().Path.ControlPoints[index].Type == type);
private void assertControlPointPosition(int index, Vector2 position) => private void assertControlPointPosition(int index, Vector2 position) =>
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position.Value, 1)); AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, getSlider().Path.ControlPoints[index].Position, 1));
private Slider getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null; private Slider getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null;

View File

@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
AddStep($"move mouse to control point {index}", () => AddStep($"move mouse to control point {index}", () =>
{ {
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value; Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position)); InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
}); });
} }

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Moq" Version="4.16.1" /> <PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />

View File

@ -10,7 +10,7 @@ using osu.Framework.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
public abstract class OsuStrainSkill : StrainSkill public abstract class OsuStrainSkill : StrainDecaySkill
{ {
/// <summary> /// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to. /// The number of sections with the highest strains, which the peak strain reductions will apply to.

View File

@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
private void updateConnectingPath() private void updateConnectingPath()
{ {
Position = slider.StackedPosition + ControlPoint.Position.Value; Position = slider.StackedPosition + ControlPoint.Position;
path.ClearVertices(); path.ClearVertices();
@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return; return;
path.AddVertex(Vector2.Zero); path.AddVertex(Vector2.Zero);
path.AddVertex(slider.Path.ControlPoints[nextIndex].Position.Value - ControlPoint.Position.Value); path.AddVertex(slider.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero); path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
} }

View File

@ -53,7 +53,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IBindable<Vector2> sliderPosition; private IBindable<Vector2> sliderPosition;
private IBindable<float> sliderScale; private IBindable<float> sliderScale;
private IBindable<Vector2> controlPointPosition;
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
{ {
@ -69,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
updatePathType(); updatePathType();
}); });
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay()); controlPoint.Changed += updateMarkerDisplay;
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -117,9 +116,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
sliderPosition = slider.PositionBindable.GetBoundCopy(); sliderPosition = slider.PositionBindable.GetBoundCopy();
sliderPosition.BindValueChanged(_ => updateMarkerDisplay()); sliderPosition.BindValueChanged(_ => updateMarkerDisplay());
controlPointPosition = ControlPoint.Position.GetBoundCopy();
controlPointPosition.BindValueChanged(_ => updateMarkerDisplay());
sliderScale = slider.ScaleBindable.GetBoundCopy(); sliderScale = slider.ScaleBindable.GetBoundCopy();
sliderScale.BindValueChanged(_ => updateMarkerDisplay()); sliderScale.BindValueChanged(_ => updateMarkerDisplay());
@ -174,8 +170,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (e.Button == MouseButton.Left) if (e.Button == MouseButton.Left)
{ {
dragStartPosition = ControlPoint.Position.Value; dragStartPosition = ControlPoint.Position;
dragPathType = PointsInSegment[0].Type.Value; dragPathType = PointsInSegment[0].Type;
changeHandler?.BeginChange(); changeHandler?.BeginChange();
return true; return true;
@ -186,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
{ {
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position.Value).ToArray(); Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
var oldPosition = slider.Position; var oldPosition = slider.Position;
var oldStartTime = slider.StartTime; var oldStartTime = slider.StartTime;
@ -202,15 +198,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta // Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < slider.Path.ControlPoints.Count; i++) for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position.Value -= movementDelta; slider.Path.ControlPoints[i].Position -= movementDelta;
} }
else else
ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition); ControlPoint.Position = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
if (!slider.Path.HasValidLength) if (!slider.Path.HasValidLength)
{ {
for (var i = 0; i < slider.Path.ControlPoints.Count; i++) for (var i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position.Value = oldControlPoints[i]; slider.Path.ControlPoints[i].Position = oldControlPoints[i];
slider.Position = oldPosition; slider.Position = oldPosition;
slider.StartTime = oldStartTime; slider.StartTime = oldStartTime;
@ -218,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
// Maintain the path type in case it got defaulted to bezier at some point during the drag. // Maintain the path type in case it got defaulted to bezier at some point during the drag.
PointsInSegment[0].Type.Value = dragPathType; PointsInSegment[0].Type = dragPathType;
} }
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
@ -230,19 +226,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
private void updatePathType() private void updatePathType()
{ {
if (ControlPoint.Type.Value != PathType.PerfectCurve) if (ControlPoint.Type != PathType.PerfectCurve)
return; return;
if (PointsInSegment.Count > 3) if (PointsInSegment.Count > 3)
ControlPoint.Type.Value = PathType.Bezier; ControlPoint.Type = PathType.Bezier;
if (PointsInSegment.Count != 3) if (PointsInSegment.Count != 3)
return; return;
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position.Value).ToArray(); ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position).ToArray();
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points); RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480) if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
ControlPoint.Type.Value = PathType.Bezier; ControlPoint.Type = PathType.Bezier;
} }
/// <summary> /// <summary>
@ -250,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
private void updateMarkerDisplay() private void updateMarkerDisplay()
{ {
Position = slider.StackedPosition + ControlPoint.Position.Value; Position = slider.StackedPosition + ControlPoint.Position;
markerRing.Alpha = IsSelected.Value ? 1 : 0; markerRing.Alpha = IsSelected.Value ? 1 : 0;
@ -265,7 +261,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private Color4 getColourFromNodeType() private Color4 getColourFromNodeType()
{ {
if (!(ControlPoint.Type.Value is PathType pathType)) if (!(ControlPoint.Type is PathType pathType))
return colours.Yellow; return colours.Yellow;
switch (pathType) switch (pathType)
@ -284,6 +280,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
} }
public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty; public LocalisableString TooltipText => ControlPoint.Type.ToString() ?? string.Empty;
} }
} }

View File

@ -173,12 +173,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
int thirdPointIndex = indexInSegment + 2; int thirdPointIndex = indexInSegment + 2;
if (piece.PointsInSegment.Count > thirdPointIndex + 1) if (piece.PointsInSegment.Count > thirdPointIndex + 1)
piece.PointsInSegment[thirdPointIndex].Type.Value = piece.PointsInSegment[0].Type.Value; piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
break; break;
} }
piece.ControlPoint.Type.Value = type; piece.ControlPoint.Type = type;
} }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private MenuItem createMenuItemForPathType(PathType? type) private MenuItem createMenuItemForPathType(PathType? type)
{ {
int totalCount = Pieces.Count(p => p.IsSelected.Value); int totalCount = Pieces.Count(p => p.IsSelected.Value);
int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type.Value == type); int countOfState = Pieces.Where(p => p.IsSelected.Value).Count(p => p.ControlPoint.Type == type);
var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ => var item = new TernaryStateRadioMenuItem(type == null ? "Inherit" : type.ToString().Humanize(), MenuItemType.Standard, _ =>
{ {

View File

@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Debug.Assert(lastPoint != null); Debug.Assert(lastPoint != null);
segmentStart = lastPoint; segmentStart = lastPoint;
segmentStart.Type.Value = PathType.Linear; segmentStart.Type = PathType.Linear;
currentSegmentLength = 1; currentSegmentLength = 1;
} }
@ -153,15 +153,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
case 1: case 1:
case 2: case 2:
segmentStart.Type.Value = PathType.Linear; segmentStart.Type = PathType.Linear;
break; break;
case 3: case 3:
segmentStart.Type.Value = PathType.PerfectCurve; segmentStart.Type = PathType.PerfectCurve;
break; break;
default: default:
segmentStart.Type.Value = PathType.Bezier; segmentStart.Type = PathType.Bezier;
break; break;
} }
} }
@ -173,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// The cursor does not overlap a previous control point, so it can be added if not already existing. // The cursor does not overlap a previous control point, so it can be added if not already existing.
if (cursor == null) if (cursor == null)
{ {
HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = { Value = Vector2.Zero } }); HitObject.Path.ControlPoints.Add(cursor = new PathControlPoint { Position = Vector2.Zero });
// The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier). // The path type should be adjusted in the progression of updatePathType() (Linear -> PC -> Bezier).
currentSegmentLength++; currentSegmentLength++;
@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
// Update the cursor position. // Update the cursor position.
cursor.Position.Value = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position; cursor.Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
} }
else if (cursor != null) else if (cursor != null)
{ {

View File

@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
Debug.Assert(placementControlPointIndex != null); Debug.Assert(placementControlPointIndex != null);
HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position.Value = e.MousePosition - HitObject.Position; HitObject.Path.ControlPoints[placementControlPointIndex.Value].Position = e.MousePosition - HitObject.Position;
} }
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)
@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
for (int i = 0; i < controlPoints.Count - 1; i++) for (int i = 0; i < controlPoints.Count - 1; i++)
{ {
float dist = new Line(controlPoints[i].Position.Value, controlPoints[i + 1].Position.Value).DistanceToPoint(position); float dist = new Line(controlPoints[i].Position, controlPoints[i + 1].Position).DistanceToPoint(position);
if (dist < minDistance) if (dist < minDistance)
{ {
@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
// Move the control points from the insertion index onwards to make room for the insertion // Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, new PathControlPoint { Position = { Value = position } }); controlPoints.Insert(insertionIndex, new PathControlPoint { Position = position });
return insertionIndex; return insertionIndex;
} }
@ -207,8 +207,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
// The first control point in the slider must have a type, so take it from the previous "first" one // The first control point in the slider must have a type, so take it from the previous "first" one
// Todo: Should be handled within SliderPath itself // Todo: Should be handled within SliderPath itself
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type.Value == null) if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null)
controlPoints[1].Type.Value = controlPoints[0].Type.Value; controlPoints[1].Type = controlPoints[0].Type;
controlPoints.Remove(c); controlPoints.Remove(c);
} }
@ -222,9 +222,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position
// So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0) // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
Vector2 first = controlPoints[0].Position.Value; Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints) foreach (var c in controlPoints)
c.Position.Value -= first; c.Position -= first;
HitObject.Position += first; HitObject.Position += first;
} }

View File

@ -41,6 +41,11 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override GameplayCursorContainer CreateCursor() => null; protected override GameplayCursorContainer CreateCursor() => null;
public OsuEditorPlayfield()
{
HitPolicy = new AnyOrderHitPolicy();
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
@ -58,6 +63,8 @@ namespace osu.Game.Rulesets.Osu.Edit
return; return;
if (hitObject is DrawableHitCircle circle) if (hitObject is DrawableHitCircle circle)
{
using (circle.BeginAbsoluteSequence(circle.HitStateUpdateTime))
{ {
circle.ApproachCircle circle.ApproachCircle
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4) .FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
@ -65,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Edit
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint); circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
} }
}
if (hitObject is IHasMainCirclePiece mainPieceContainer) if (hitObject is IHasMainCirclePiece mainPieceContainer)
{ {

View File

@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
foreach (var point in slider.Path.ControlPoints) foreach (var point in slider.Path.ControlPoints)
{ {
point.Position.Value = new Vector2( point.Position = new Vector2(
(direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X, (direction == Direction.Horizontal ? -1 : 1) * point.Position.X,
(direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y (direction == Direction.Vertical ? -1 : 1) * point.Position.Y
); );
} }
} }
@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (h is IHasPath path) if (h is IHasPath path)
{ {
foreach (var point in path.Path.ControlPoints) foreach (var point in path.Path.ControlPoints)
point.Position.Value = RotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta); point.Position = RotatePointAroundOrigin(point.Position, Vector2.Zero, delta);
} }
} }
@ -163,9 +163,9 @@ namespace osu.Game.Rulesets.Osu.Edit
private void scaleSlider(Slider slider, Vector2 scale) private void scaleSlider(Slider slider, Vector2 scale)
{ {
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList(); referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0. // Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size; scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
@ -178,13 +178,13 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var point in slider.Path.ControlPoints) foreach (var point in slider.Path.ControlPoints)
{ {
oldControlPoints.Enqueue(point.Position.Value); oldControlPoints.Enqueue(point.Position);
point.Position.Value *= pathRelativeDeltaScale; point.Position *= pathRelativeDeltaScale;
} }
// Maintain the path types in case they were defaulted to bezier at some point during scaling // Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i) for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
slider.Path.ControlPoints[i].Type.Value = referencePathTypes[i]; slider.Path.ControlPoints[i].Type = referencePathTypes[i];
//if sliderhead or sliderend end up outside playfield, revert scaling. //if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider }); Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Edit
return; return;
foreach (var point in slider.Path.ControlPoints) foreach (var point in slider.Path.ControlPoints)
point.Position.Value = oldControlPoints.Dequeue(); point.Position = oldControlPoints.Dequeue();
} }
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale) private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Osu.Edit.Setup
{
public class OsuSetupSection : RulesetSetupSection
{
private LabelledSliderBar<float> stackLeniency;
public OsuSetupSection()
: base(new OsuRuleset().RulesetInfo)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new[]
{
stackLeniency = new LabelledSliderBar<float>
{
Label = "Stack Leniency",
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
{
Default = 0.7f,
MinValue = 0,
MaxValue = 1,
Precision = 0.1f
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
stackLeniency.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateBeatmap()
{
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
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.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield.HitObjectContainer, drawableRuleset.Beatmap)); drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield, drawableRuleset.Beatmap));
} }
public void ApplyToHealthProcessor(HealthProcessor healthProcessor) public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
@ -128,8 +129,21 @@ namespace osu.Game.Rulesets.Osu.Mods
protected override void Update() protected override void Update()
{ {
float start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X; float start, end;
float end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
if (Precision.AlmostEquals(restrictTo.Rotation, 0))
{
start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
}
else
{
float center = restrictTo.ToSpaceOfOtherDrawable(restrictTo.OriginPosition, Parent).X;
float halfDiagonal = (restrictTo.DrawSize / 2).LengthFast;
start = center - halfDiagonal;
end = center + halfDiagonal;
}
float rawWidth = end - start; float rawWidth = end - start;

View File

@ -4,6 +4,7 @@
#nullable enable #nullable enable
using System; using System;
using System.Diagnostics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -20,8 +21,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
Start = start; Start = start;
LifetimeStart = Start.StartTime; LifetimeStart = Start.StartTime;
bindEvents();
} }
private OsuHitObject? end; private OsuHitObject? end;
@ -41,31 +40,39 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
} }
} }
private bool wasBound;
private void bindEvents() private void bindEvents()
{ {
UnbindEvents(); UnbindEvents();
if (End == null)
return;
// Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects. // Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects.
Start.DefaultsApplied += onDefaultsApplied; Start.DefaultsApplied += onDefaultsApplied;
Start.PositionBindable.ValueChanged += onPositionChanged; Start.PositionBindable.ValueChanged += onPositionChanged;
if (End != null)
{
End.DefaultsApplied += onDefaultsApplied; End.DefaultsApplied += onDefaultsApplied;
End.PositionBindable.ValueChanged += onPositionChanged; End.PositionBindable.ValueChanged += onPositionChanged;
}
wasBound = true;
} }
public void UnbindEvents() public void UnbindEvents()
{ {
if (!wasBound)
return;
Debug.Assert(End != null);
Start.DefaultsApplied -= onDefaultsApplied; Start.DefaultsApplied -= onDefaultsApplied;
Start.PositionBindable.ValueChanged -= onPositionChanged; Start.PositionBindable.ValueChanged -= onPositionChanged;
if (End != null)
{
End.DefaultsApplied -= onDefaultsApplied; End.DefaultsApplied -= onDefaultsApplied;
End.PositionBindable.ValueChanged -= onPositionChanged; End.PositionBindable.ValueChanged -= onPositionChanged;
}
wasBound = false;
} }
private void onDefaultsApplied(HitObject obj) => refreshLifetimes(); private void onDefaultsApplied(HitObject obj) => refreshLifetimes();

View File

@ -74,10 +74,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override void PlayAnimation() public override void PlayAnimation()
{ {
base.PlayAnimation();
if (Result != HitResult.Miss) if (Result != HitResult.Miss)
JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); {
JudgementText
.ScaleTo(new Vector2(0.8f, 1))
.ScaleTo(new Vector2(1.2f, 1), 1800, Easing.OutQuint);
}
base.PlayAnimation();
} }
} }
} }

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Objects
if (value != null) if (value != null)
{ {
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position.Value, c.Type.Value))); path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
path.ExpectedDistance.Value = value.ExpectedDistance.Value; path.ExpectedDistance.Value = value.ExpectedDistance.Value;
} }
} }

View File

@ -30,9 +30,11 @@ using osu.Game.Skinning;
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Osu.Edit.Setup;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
@ -305,5 +307,7 @@ namespace osu.Game.Rulesets.Osu
} }
}; };
} }
public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();
} }
} }

View File

@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
pathVersion.BindValueChanged(_ => Refresh()); pathVersion.BindValueChanged(_ => Refresh());
accentColour = drawableObject.AccentColour.GetBoundCopy(); accentColour = drawableObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true); accentColour.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn); config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut); config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
} }
} }
private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour) protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) =>
=> AccentColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderTrackOverride)?.Value ?? defaultAccentColour; skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderTrackOverride)?.Value ?? hitObjectAccentColour;
} }
} }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// <summary> /// <summary>
/// A <see cref="SliderBody"/> which changes its curve depending on the snaking progress. /// A <see cref="SliderBody"/> which changes its curve depending on the snaking progress.
/// </summary> /// </summary>
public class SnakingSliderBody : SliderBody, ISliderProgress public abstract class SnakingSliderBody : SliderBody, ISliderProgress
{ {
public readonly List<Vector2> CurrentCurve = new List<Vector2>(); public readonly List<Vector2> CurrentCurve = new List<Vector2>();

View File

@ -9,6 +9,7 @@ using osu.Framework.Input.Events;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private double lastTrailTime; private double lastTrailTime;
private IBindable<float> cursorSize; private IBindable<float> cursorSize;
private Vector2? currentPosition;
public LegacyCursorTrail(ISkin skin) public LegacyCursorTrail(ISkin skin)
{ {
this.skin = skin; this.skin = skin;
@ -54,22 +57,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
} }
protected override double FadeDuration => disjointTrail ? 150 : 500; protected override double FadeDuration => disjointTrail ? 150 : 500;
protected override float FadeExponent => 1;
protected override bool InterpolateMovements => !disjointTrail; protected override bool InterpolateMovements => !disjointTrail;
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
protected override bool AvoidDrawingNearCursor => !disjointTrail;
protected override void Update()
{
base.Update();
if (!disjointTrail || !currentPosition.HasValue)
return;
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
{
lastTrailTime = Time.Current;
AddTrail(currentPosition.Value);
}
}
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
if (!disjointTrail) if (!disjointTrail)
return base.OnMouseMove(e); return base.OnMouseMove(e);
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation) currentPosition = e.ScreenSpaceMousePosition;
{
lastTrailTime = Time.Current;
return base.OnMouseMove(e);
}
// Intentionally block the base call as we're adding the trails ourselves.
return false; return false;
} }
} }

View File

@ -3,9 +3,10 @@
using System; using System;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
@ -14,6 +15,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
protected override DrawableSliderPath CreateSliderPath() => new LegacyDrawableSliderPath(); protected override DrawableSliderPath CreateSliderPath() => new LegacyDrawableSliderPath();
protected override Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour)
{
// legacy skins use a constant value for slider track alpha, regardless of the source colour.
return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(0.7f);
}
private class LegacyDrawableSliderPath : DrawableSliderPath private class LegacyDrawableSliderPath : DrawableSliderPath
{ {
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS); private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
@ -22,8 +29,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// Roughly matches osu!stable's slider border portions. // Roughly matches osu!stable's slider border portions.
=> base.CalculatedBorderPortion * 0.77f; => base.CalculatedBorderPortion * 0.77f;
public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f);
protected override Color4 ColourAt(float position) protected override Color4 ColourAt(float position)
{ {
float realBorderPortion = shadow_portion + CalculatedBorderPortion; float realBorderPortion = shadow_portion + CalculatedBorderPortion;
@ -40,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Color4 outerColour = AccentColour.Darken(0.1f); Color4 outerColour = AccentColour.Darken(0.1f);
Color4 innerColour = lighten(AccentColour, 0.5f); Color4 innerColour = lighten(AccentColour, 0.5f);
return Interpolation.ValueAt(position / realGradientPortion, outerColour, innerColour, 0, 1); return LegacyUtils.InterpolateNonLinear(position / realGradientPortion, outerColour, innerColour, 0, 1);
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// An <see cref="IHitPolicy"/> which allows hitobjects to be hit in any order.
/// </summary>
public class AnyOrderHitPolicy : IHitPolicy
{
public IHitObjectContainer HitObjectContainer { get; set; }
public bool IsHittable(DrawableHitObject hitObject, double time) => true;
public void HandleHit(DrawableHitObject hitObject)
{
}
}
}

View File

@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
private const int max_sprites = 2048; private const int max_sprites = 2048;
/// <summary>
/// An exponentiating factor to ease the trail fade.
/// </summary>
protected virtual float FadeExponent => 1.7f;
private readonly TrailPart[] parts = new TrailPart[max_sprites]; private readonly TrailPart[] parts = new TrailPart[max_sprites];
private int currentIndex; private int currentIndex;
private IShader shader; private IShader shader;
@ -133,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected virtual bool InterpolateMovements => true; protected virtual bool InterpolateMovements => true;
protected virtual float IntervalMultiplier => 1.0f; protected virtual float IntervalMultiplier => 1.0f;
protected virtual bool AvoidDrawingNearCursor => false;
private Vector2? lastPosition; private Vector2? lastPosition;
private readonly InputResampler resampler = new InputResampler(); private readonly InputResampler resampler = new InputResampler();
@ -141,45 +147,47 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
Vector2 pos = e.ScreenSpaceMousePosition; AddTrail(e.ScreenSpaceMousePosition);
if (lastPosition == null)
{
lastPosition = pos;
resampler.AddPosition(lastPosition.Value);
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
foreach (Vector2 pos2 in resampler.AddPosition(pos)) protected void AddTrail(Vector2 position)
{
if (InterpolateMovements)
{
if (!lastPosition.HasValue)
{
lastPosition = position;
resampler.AddPosition(lastPosition.Value);
return;
}
foreach (Vector2 pos2 in resampler.AddPosition(position))
{ {
Trace.Assert(lastPosition.HasValue); Trace.Assert(lastPosition.HasValue);
if (InterpolateMovements)
{
// ReSharper disable once PossibleInvalidOperationException
Vector2 pos1 = lastPosition.Value; Vector2 pos1 = lastPosition.Value;
Vector2 diff = pos2 - pos1; Vector2 diff = pos2 - pos1;
float distance = diff.Length; float distance = diff.Length;
Vector2 direction = diff / distance; Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier; float interval = partSize.X / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
for (float d = interval; d < distance; d += interval) for (float d = interval; d < stopAt; d += interval)
{ {
lastPosition = pos1 + direction * d; lastPosition = pos1 + direction * d;
addPart(lastPosition.Value); addPart(lastPosition.Value);
} }
} }
}
else else
{ {
lastPosition = pos2; lastPosition = position;
addPart(lastPosition.Value); addPart(lastPosition.Value);
} }
} }
return base.OnMouseMove(e);
}
private void addPart(Vector2 screenSpacePosition) private void addPart(Vector2 screenSpacePosition)
{ {
parts[currentIndex].Position = screenSpacePosition; parts[currentIndex].Position = screenSpacePosition;
@ -206,10 +214,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Texture texture; private Texture texture;
private float time; private float time;
private float fadeExponent;
private readonly TrailPart[] parts = new TrailPart[max_sprites]; private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size; private Vector2 size;
private Vector2 originPosition; private Vector2 originPosition;
private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1); private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1);
@ -227,6 +235,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
texture = Source.texture; texture = Source.texture;
size = Source.partSize; size = Source.partSize;
time = Source.time; time = Source.time;
fadeExponent = Source.FadeExponent;
originPosition = Vector2.Zero; originPosition = Vector2.Zero;
@ -249,6 +258,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
shader.Bind(); shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time); shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
texture.TextureGL.Bind(); texture.TextureGL.Bind();

View File

@ -119,9 +119,9 @@ namespace osu.Game.Rulesets.Osu.Utils
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y)); slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y)); slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray(); var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints) foreach (var point in controlPoints)
point.Position.Value = new Vector2(-point.Position.Value.X, point.Position.Value.Y); point.Position = new Vector2(-point.Position.X, point.Position.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
} }
@ -140,9 +140,9 @@ namespace osu.Game.Rulesets.Osu.Utils
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray(); var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
foreach (var point in controlPoints) foreach (var point in controlPoints)
point.Position.Value = new Vector2(point.Position.Value.X, -point.Position.Value.Y); point.Position = new Vector2(point.Position.X, -point.Position.Y);
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value); slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
} }

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Origin = Anchor.Centre, Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
{ {
new TaikoPlayfield(new ControlPointInfo()), new TaikoPlayfield(),
hoc = new ScrollingHitObjectContainer() hoc = new ScrollingHitObjectContainer()
} }
}; };
@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Origin = Anchor.Centre, Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
{ {
new TaikoPlayfield(new ControlPointInfo()), new TaikoPlayfield(),
hoc = new ScrollingHitObjectContainer() hoc = new ScrollingHitObjectContainer()
} }
}; };

View File

@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
using osuTK; using osuTK;
@ -17,6 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var playfield = new TaikoPlayfield();
var beatmap = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo).GetPlayableBeatmap(new TaikoRuleset().RulesetInfo);
foreach (var h in beatmap.HitObjects)
playfield.Add(h);
SetContents(_ => new TaikoInputManager(new TaikoRuleset().RulesetInfo) SetContents(_ => new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -25,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(200), Size = new Vector2(200),
Child = new InputDrum(new ControlPointInfo()) Child = new InputDrum(playfield.HitObjectContainer)
} }
}); });
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Beatmap.Value.Track.Start(); Beatmap.Value.Track.Start();
}); });
AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield(new ControlPointInfo()) AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -1,104 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Audio
{
/// <summary>
/// Stores samples for the input drum.
/// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point.
/// </summary>
public class DrumSampleContainer : LifetimeManagementContainer
{
private readonly ControlPointInfo controlPoints;
private readonly Dictionary<double, DrumSample> mappings = new Dictionary<double, DrumSample>();
private readonly IBindableList<SampleControlPoint> samplePoints = new BindableList<SampleControlPoint>();
public DrumSampleContainer(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
}
[BackgroundDependencyLoader]
private void load()
{
samplePoints.BindTo(controlPoints.SamplePoints);
samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true);
}
private void recreateMappings()
{
mappings.Clear();
ClearInternal();
SampleControlPoint[] points = samplePoints.Count == 0
? new[] { controlPoints.SamplePointAt(double.MinValue) }
: samplePoints.ToArray();
for (int i = 0; i < points.Length; i++)
{
var samplePoint = points[i];
var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue;
var lifetimeEnd = i + 1 < points.Length ? points[i + 1].Time : double.MaxValue;
AddInternal(mappings[samplePoint.Time] = new DrumSample(samplePoint)
{
LifetimeStart = lifetimeStart,
LifetimeEnd = lifetimeEnd
});
}
}
public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
public class DrumSample : CompositeDrawable
{
public override bool RemoveWhenNotAlive => false;
public PausableSkinnableSound Centre { get; private set; }
public PausableSkinnableSound Rim { get; private set; }
private readonly SampleControlPoint samplePoint;
private Bindable<string> sampleBank;
private BindableNumber<int> sampleVolume;
public DrumSample(SampleControlPoint samplePoint)
{
this.samplePoint = samplePoint;
}
[BackgroundDependencyLoader]
private void load()
{
sampleBank = samplePoint.SampleBankBindable.GetBoundCopy();
sampleBank.BindValueChanged(_ => recreate());
sampleVolume = samplePoint.SampleVolumeBindable.GetBoundCopy();
sampleVolume.BindValueChanged(_ => recreate());
recreate();
}
private void recreate()
{
InternalChildren = new Drawable[]
{
Centre = new PausableSkinnableSound(samplePoint.GetSampleInfo()),
Rim = new PausableSkinnableSound(samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP))
};
}
}
}
}

View File

@ -18,12 +18,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{ {
internal class TaikoBeatmapConverter : BeatmapConverter<TaikoHitObject> internal class TaikoBeatmapConverter : BeatmapConverter<TaikoHitObject>
{ {
/// <summary>
/// osu! is generally slower than taiko, so a factor is added to increase
/// speed. This must be used everywhere slider length or beat length is used.
/// </summary>
public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f;
/// <summary> /// <summary>
/// Because swells are easier in taiko than spinners are in osu!, /// Because swells are easier in taiko than spinners are in osu!,
/// legacy taiko multiplies a factor when converting the number of required hits. /// legacy taiko multiplies a factor when converting the number of required hits.
@ -51,11 +45,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
public override bool CanConvert() => true; public override bool CanConvert() => true;
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
if (!(original.BeatmapInfo.BaseDifficulty is TaikoMutliplierAppliedDifficulty))
{ {
// Rewrite the beatmap info to add the slider velocity multiplier // Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone(); original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone(); original.BeatmapInfo.BaseDifficulty = new TaikoMutliplierAppliedDifficulty(original.BeatmapInfo.BaseDifficulty);
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER; }
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken); Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
@ -155,7 +151,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// The true distance, accounting for any repeats. This ends up being the drum roll distance later // The true distance, accounting for any repeats. This ends up being the drum roll distance later
int spans = (obj as IHasRepeats)?.SpanCount() ?? 1; int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER; double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime); DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
@ -194,5 +190,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
} }
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap(); protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty
{
public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty)
{
difficulty.CopyTo(this);
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
}
} }
} }

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <summary> /// <summary>
/// Calculates the colour coefficient of taiko difficulty. /// Calculates the colour coefficient of taiko difficulty.
/// </summary> /// </summary>
public class Colour : StrainSkill public class Colour : StrainDecaySkill
{ {
protected override double SkillMultiplier => 1; protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4; protected override double StrainDecayBase => 0.4;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <summary> /// <summary>
/// Calculates the rhythm coefficient of taiko difficulty. /// Calculates the rhythm coefficient of taiko difficulty.
/// </summary> /// </summary>
public class Rhythm : StrainSkill public class Rhythm : StrainDecaySkill
{ {
protected override double SkillMultiplier => 10; protected override double SkillMultiplier => 10;
protected override double StrainDecayBase => 0; protected override double StrainDecayBase => 0;

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <remarks> /// <remarks>
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit). /// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
/// </remarks> /// </remarks>
public class Stamina : StrainSkill public class Stamina : StrainDecaySkill
{ {
protected override double SkillMultiplier => 1; protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4; protected override double StrainDecayBase => 0.4;

View File

@ -6,10 +6,10 @@ using System;
using System.Threading; using System.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Judgements;
using osuTK; using osuTK;
@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
double IHasDistance.Distance => Duration * Velocity; double IHasDistance.Distance => Duration * Velocity;
SliderPath IHasPath.Path SliderPath IHasPath.Path
=> new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER); => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER);
#endregion #endregion
} }

View File

@ -7,7 +7,8 @@ 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.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Taiko.Audio; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -111,7 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public readonly Sprite Centre; public readonly Sprite Centre;
[Resolved] [Resolved]
private DrumSampleContainer sampleContainer { get; set; } private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public LegacyHalfDrum(bool flipped) public LegacyHalfDrum(bool flipped)
{ {
@ -143,17 +144,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public bool OnPressed(TaikoAction action) public bool OnPressed(TaikoAction action)
{ {
Drawable target = null; Drawable target = null;
var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction) if (action == CentreAction)
{ {
target = Centre; target = Centre;
drumSample.Centre?.Play(); sampleTriggerSource.Play(HitType.Centre);
} }
else if (action == RimAction) else if (action == RimAction)
{ {
target = Rim; target = Rim;
drumSample.Rim?.Play(); sampleTriggerSource.Play(HitType.Rim);
} }
if (target != null) if (target != null)

View File

@ -3,6 +3,7 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.UI namespace osu.Game.Rulesets.Taiko.UI
{ {
@ -16,5 +17,26 @@ namespace osu.Game.Rulesets.Taiko.UI
this.MoveToY(-100, 500); this.MoveToY(-100, 500);
base.ApplyHitAnimations(); base.ApplyHitAnimations();
} }
protected override Drawable CreateDefaultJudgement(HitResult result) => new TaikoJudgementPiece(result);
private class TaikoJudgementPiece : DefaultJudgementPiece
{
public TaikoJudgementPiece(HitResult result)
: base(result)
{
}
public override void PlayAnimation()
{
if (Result != HitResult.Miss)
{
JudgementText.ScaleTo(0.9f);
JudgementText.ScaleTo(1, 500, Easing.OutElastic);
}
base.PlayAnimation();
}
}
} }
} }

View File

@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo); protected override Playfield CreatePlayfield() => new TaikoPlayfield();
public override DrawableHitObject<TaikoHitObject> CreateDrawableRepresentation(TaikoHitObject h) => null; public override DrawableHitObject<TaikoHitObject> CreateDrawableRepresentation(TaikoHitObject h) => null;

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.UI
{
public class DrumSampleTriggerSource : GameplaySampleTriggerSource
{
public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer)
: base(hitObjectContainer)
{
}
public void Play(HitType hitType)
{
var hitObject = GetMostValidObject();
if (hitObject == null)
return;
PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) });
}
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
}
}

View File

@ -2,18 +2,18 @@
// 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 osuTK;
using osu.Framework.Allocation; using osu.Framework.Allocation;
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.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Audio; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI namespace osu.Game.Rulesets.Taiko.UI
{ {
@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float middle_split = 0.025f; private const float middle_split = 0.025f;
[Cached] [Cached]
private DrumSampleContainer sampleContainer; private DrumSampleTriggerSource sampleTriggerSource;
public InputDrum(ControlPointInfo controlPoints) public InputDrum(HitObjectContainer hitObjectContainer)
{ {
sampleContainer = new DrumSampleContainer(controlPoints); sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.UI
} }
} }
}), }),
sampleContainer sampleTriggerSource
}; };
} }
@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centreHit; private readonly Sprite centreHit;
[Resolved] [Resolved]
private DrumSampleContainer sampleContainer { get; set; } private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public TaikoHalfDrum(bool flipped) public TaikoHalfDrum(bool flipped)
{ {
@ -156,21 +156,19 @@ namespace osu.Game.Rulesets.Taiko.UI
Drawable target = null; Drawable target = null;
Drawable back = null; Drawable back = null;
var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction) if (action == CentreAction)
{ {
target = centreHit; target = centreHit;
back = centre; back = centre;
drumSample.Centre?.Play(); sampleTriggerSource.Play(HitType.Centre);
} }
else if (action == RimAction) else if (action == RimAction)
{ {
target = rimHit; target = rimHit;
back = rim; back = rim;
drumSample.Rim?.Play(); sampleTriggerSource.Play(HitType.Rim);
} }
if (target != null) if (target != null)

View File

@ -8,7 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -27,8 +26,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{ {
public class TaikoPlayfield : ScrollingPlayfield public class TaikoPlayfield : ScrollingPlayfield
{ {
private readonly ControlPointInfo controlPoints;
/// <summary> /// <summary>
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>. /// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
/// </summary> /// </summary>
@ -56,11 +53,6 @@ namespace osu.Game.Rulesets.Taiko.UI
private Container hitTargetOffsetContent; private Container hitTargetOffsetContent;
public TaikoPlayfield(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -131,7 +123,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Children = new Drawable[] Children = new Drawable[]
{ {
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()), new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
new InputDrum(controlPoints) new InputDrum(HitObjectContainer)
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,

View File

@ -1,56 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
public class BeatmapDifficultyCacheTest
{
[Test]
public void TestKeyEqualsWithDifferentModInstances()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
Assert.That(key1, Is.EqualTo(key2));
}
[Test]
public void TestKeyEqualsWithDifferentModOrder()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
Assert.That(key1, Is.EqualTo(key2));
}
[TestCase(1.3, DifficultyRating.Easy)]
[TestCase(1.993, DifficultyRating.Easy)]
[TestCase(1.998, DifficultyRating.Normal)]
[TestCase(2.4, DifficultyRating.Normal)]
[TestCase(2.693, DifficultyRating.Normal)]
[TestCase(2.698, DifficultyRating.Hard)]
[TestCase(3.5, DifficultyRating.Hard)]
[TestCase(3.993, DifficultyRating.Hard)]
[TestCase(3.997, DifficultyRating.Insane)]
[TestCase(5.0, DifficultyRating.Insane)]
[TestCase(5.292, DifficultyRating.Insane)]
[TestCase(5.297, DifficultyRating.Expert)]
[TestCase(6.2, DifficultyRating.Expert)]
[TestCase(6.493, DifficultyRating.Expert)]
[TestCase(6.498, DifficultyRating.ExpertPlus)]
[TestCase(8.3, DifficultyRating.ExpertPlus)]
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
{
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
Assert.AreEqual(expectedBracket, actualBracket);
}
}
}

View File

@ -3,15 +3,12 @@
using System; using System;
using System.IO; using System.IO;
using NUnit.Framework;
using osuTK;
using osuTK.Graphics;
using osu.Game.Tests.Resources;
using System.Linq; using System.Linq;
using NUnit.Framework;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
@ -19,9 +16,13 @@ using osu.Game.Rulesets.Catch.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.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
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.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Beatmaps.Formats namespace osu.Game.Tests.Beatmaps.Formats
{ {
@ -58,12 +59,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile);
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
Assert.AreEqual(164471, metadata.PreviewTime); Assert.AreEqual(164471, metadata.PreviewTime);
Assert.IsFalse(beatmapInfo.Countdown);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
Assert.IsTrue(beatmapInfo.RulesetID == 0); Assert.IsTrue(beatmapInfo.RulesetID == 0);
Assert.IsFalse(beatmapInfo.LetterboxInBreaks); Assert.IsFalse(beatmapInfo.LetterboxInBreaks);
Assert.IsFalse(beatmapInfo.SpecialStyle); Assert.IsFalse(beatmapInfo.SpecialStyle);
Assert.IsFalse(beatmapInfo.WidescreenStoryboard); Assert.IsFalse(beatmapInfo.WidescreenStoryboard);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset);
} }
} }
@ -165,7 +167,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
{ {
var beatmap = decoder.Decode(stream); var beatmap = decoder.Decode(stream);
var controlPoints = beatmap.ControlPointInfo; var controlPoints = (LegacyControlPointInfo)beatmap.ControlPointInfo;
Assert.AreEqual(4, controlPoints.TimingPoints.Count); Assert.AreEqual(4, controlPoints.TimingPoints.Count);
Assert.AreEqual(5, controlPoints.DifficultyPoints.Count); Assert.AreEqual(5, controlPoints.DifficultyPoints.Count);
@ -239,7 +241,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("overlapping-control-points.osu")) using (var resStream = TestResources.OpenResource("overlapping-control-points.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
{ {
var controlPoints = decoder.Decode(stream).ControlPointInfo; var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(4)); Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(4));
Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3)); Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3));
@ -665,111 +667,111 @@ namespace osu.Game.Tests.Beatmaps.Formats
// Multi-segment // Multi-segment
var first = ((IHasPath)decoded.HitObjects[0]).Path; var first = ((IHasPath)decoded.HitObjects[0]).Path;
Assert.That(first.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); Assert.That(first.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(first.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve)); Assert.That(first.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
Assert.That(first.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244))); Assert.That(first.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
Assert.That(first.ControlPoints[1].Type.Value, Is.EqualTo(null)); Assert.That(first.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(first.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3))); Assert.That(first.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
Assert.That(first.ControlPoints[2].Type.Value, Is.EqualTo(PathType.Bezier)); Assert.That(first.ControlPoints[2].Type, Is.EqualTo(PathType.Bezier));
Assert.That(first.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(68, 15))); Assert.That(first.ControlPoints[3].Position, Is.EqualTo(new Vector2(68, 15)));
Assert.That(first.ControlPoints[3].Type.Value, Is.EqualTo(null)); Assert.That(first.ControlPoints[3].Type, Is.EqualTo(null));
Assert.That(first.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(259, -132))); Assert.That(first.ControlPoints[4].Position, Is.EqualTo(new Vector2(259, -132)));
Assert.That(first.ControlPoints[4].Type.Value, Is.EqualTo(null)); Assert.That(first.ControlPoints[4].Type, Is.EqualTo(null));
Assert.That(first.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(92, -107))); Assert.That(first.ControlPoints[5].Position, Is.EqualTo(new Vector2(92, -107)));
Assert.That(first.ControlPoints[5].Type.Value, Is.EqualTo(null)); Assert.That(first.ControlPoints[5].Type, Is.EqualTo(null));
// Single-segment // Single-segment
var second = ((IHasPath)decoded.HitObjects[1]).Path; var second = ((IHasPath)decoded.HitObjects[1]).Path;
Assert.That(second.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); Assert.That(second.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(second.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve)); Assert.That(second.ControlPoints[0].Type, Is.EqualTo(PathType.PerfectCurve));
Assert.That(second.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244))); Assert.That(second.ControlPoints[1].Position, Is.EqualTo(new Vector2(161, -244)));
Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null)); Assert.That(second.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3))); Assert.That(second.ControlPoints[2].Position, Is.EqualTo(new Vector2(376, -3)));
Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null)); Assert.That(second.ControlPoints[2].Type, Is.EqualTo(null));
// Implicit multi-segment // Implicit multi-segment
var third = ((IHasPath)decoded.HitObjects[2]).Path; var third = ((IHasPath)decoded.HitObjects[2]).Path;
Assert.That(third.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); Assert.That(third.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(third.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier)); Assert.That(third.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(0, 192))); Assert.That(third.ControlPoints[1].Position, Is.EqualTo(new Vector2(0, 192)));
Assert.That(third.ControlPoints[1].Type.Value, Is.EqualTo(null)); Assert.That(third.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(224, 192))); Assert.That(third.ControlPoints[2].Position, Is.EqualTo(new Vector2(224, 192)));
Assert.That(third.ControlPoints[2].Type.Value, Is.EqualTo(null)); Assert.That(third.ControlPoints[2].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(224, 0))); Assert.That(third.ControlPoints[3].Position, Is.EqualTo(new Vector2(224, 0)));
Assert.That(third.ControlPoints[3].Type.Value, Is.EqualTo(PathType.Bezier)); Assert.That(third.ControlPoints[3].Type, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(224, -192))); Assert.That(third.ControlPoints[4].Position, Is.EqualTo(new Vector2(224, -192)));
Assert.That(third.ControlPoints[4].Type.Value, Is.EqualTo(null)); Assert.That(third.ControlPoints[4].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(480, -192))); Assert.That(third.ControlPoints[5].Position, Is.EqualTo(new Vector2(480, -192)));
Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null)); Assert.That(third.ControlPoints[5].Type, Is.EqualTo(null));
Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0))); Assert.That(third.ControlPoints[6].Position, Is.EqualTo(new Vector2(480, 0)));
Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null)); Assert.That(third.ControlPoints[6].Type, Is.EqualTo(null));
// Last control point duplicated // Last control point duplicated
var fourth = ((IHasPath)decoded.HitObjects[3]).Path; var fourth = ((IHasPath)decoded.HitObjects[3]).Path;
Assert.That(fourth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); Assert.That(fourth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(fourth.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier)); Assert.That(fourth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
Assert.That(fourth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(1, 1))); Assert.That(fourth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
Assert.That(fourth.ControlPoints[1].Type.Value, Is.EqualTo(null)); Assert.That(fourth.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(fourth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(2, 2))); Assert.That(fourth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
Assert.That(fourth.ControlPoints[2].Type.Value, Is.EqualTo(null)); Assert.That(fourth.ControlPoints[2].Type, Is.EqualTo(null));
Assert.That(fourth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(3, 3))); Assert.That(fourth.ControlPoints[3].Position, Is.EqualTo(new Vector2(3, 3)));
Assert.That(fourth.ControlPoints[3].Type.Value, Is.EqualTo(null)); Assert.That(fourth.ControlPoints[3].Type, Is.EqualTo(null));
Assert.That(fourth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(3, 3))); Assert.That(fourth.ControlPoints[4].Position, Is.EqualTo(new Vector2(3, 3)));
Assert.That(fourth.ControlPoints[4].Type.Value, Is.EqualTo(null)); Assert.That(fourth.ControlPoints[4].Type, Is.EqualTo(null));
// Last control point in segment duplicated // Last control point in segment duplicated
var fifth = ((IHasPath)decoded.HitObjects[4]).Path; var fifth = ((IHasPath)decoded.HitObjects[4]).Path;
Assert.That(fifth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); Assert.That(fifth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(fifth.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier)); Assert.That(fifth.ControlPoints[0].Type, Is.EqualTo(PathType.Bezier));
Assert.That(fifth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(1, 1))); Assert.That(fifth.ControlPoints[1].Position, Is.EqualTo(new Vector2(1, 1)));
Assert.That(fifth.ControlPoints[1].Type.Value, Is.EqualTo(null)); Assert.That(fifth.ControlPoints[1].Type, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(2, 2))); Assert.That(fifth.ControlPoints[2].Position, Is.EqualTo(new Vector2(2, 2)));
Assert.That(fifth.ControlPoints[2].Type.Value, Is.EqualTo(null)); Assert.That(fifth.ControlPoints[2].Type, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(3, 3))); Assert.That(fifth.ControlPoints[3].Position, Is.EqualTo(new Vector2(3, 3)));
Assert.That(fifth.ControlPoints[3].Type.Value, Is.EqualTo(null)); Assert.That(fifth.ControlPoints[3].Type, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(3, 3))); Assert.That(fifth.ControlPoints[4].Position, Is.EqualTo(new Vector2(3, 3)));
Assert.That(fifth.ControlPoints[4].Type.Value, Is.EqualTo(null)); Assert.That(fifth.ControlPoints[4].Type, Is.EqualTo(null));
Assert.That(fifth.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(4, 4))); Assert.That(fifth.ControlPoints[5].Position, Is.EqualTo(new Vector2(4, 4)));
Assert.That(fifth.ControlPoints[5].Type.Value, Is.EqualTo(PathType.Bezier)); Assert.That(fifth.ControlPoints[5].Type, Is.EqualTo(PathType.Bezier));
Assert.That(fifth.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(5, 5))); Assert.That(fifth.ControlPoints[6].Position, Is.EqualTo(new Vector2(5, 5)));
Assert.That(fifth.ControlPoints[6].Type.Value, Is.EqualTo(null)); Assert.That(fifth.ControlPoints[6].Type, Is.EqualTo(null));
// Implicit perfect-curve multi-segment(Should convert to bezier to match stable) // Implicit perfect-curve multi-segment(Should convert to bezier to match stable)
var sixth = ((IHasPath)decoded.HitObjects[5]).Path; var sixth = ((IHasPath)decoded.HitObjects[5]).Path;
Assert.That(sixth.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); Assert.That(sixth.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(sixth.ControlPoints[0].Type.Value == PathType.Bezier); Assert.That(sixth.ControlPoints[0].Type == PathType.Bezier);
Assert.That(sixth.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(75, 145))); Assert.That(sixth.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
Assert.That(sixth.ControlPoints[1].Type.Value == null); Assert.That(sixth.ControlPoints[1].Type == null);
Assert.That(sixth.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(170, 75))); Assert.That(sixth.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
Assert.That(sixth.ControlPoints[2].Type.Value == PathType.Bezier); Assert.That(sixth.ControlPoints[2].Type == PathType.Bezier);
Assert.That(sixth.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(300, 145))); Assert.That(sixth.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
Assert.That(sixth.ControlPoints[3].Type.Value == null); Assert.That(sixth.ControlPoints[3].Type == null);
Assert.That(sixth.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(410, 20))); Assert.That(sixth.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
Assert.That(sixth.ControlPoints[4].Type.Value == null); Assert.That(sixth.ControlPoints[4].Type == null);
// Explicit perfect-curve multi-segment(Should not convert to bezier) // Explicit perfect-curve multi-segment(Should not convert to bezier)
var seventh = ((IHasPath)decoded.HitObjects[6]).Path; var seventh = ((IHasPath)decoded.HitObjects[6]).Path;
Assert.That(seventh.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); Assert.That(seventh.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
Assert.That(seventh.ControlPoints[0].Type.Value == PathType.PerfectCurve); Assert.That(seventh.ControlPoints[0].Type == PathType.PerfectCurve);
Assert.That(seventh.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(75, 145))); Assert.That(seventh.ControlPoints[1].Position, Is.EqualTo(new Vector2(75, 145)));
Assert.That(seventh.ControlPoints[1].Type.Value == null); Assert.That(seventh.ControlPoints[1].Type == null);
Assert.That(seventh.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(170, 75))); Assert.That(seventh.ControlPoints[2].Position, Is.EqualTo(new Vector2(170, 75)));
Assert.That(seventh.ControlPoints[2].Type.Value == PathType.PerfectCurve); Assert.That(seventh.ControlPoints[2].Type == PathType.PerfectCurve);
Assert.That(seventh.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(300, 145))); Assert.That(seventh.ControlPoints[3].Position, Is.EqualTo(new Vector2(300, 145)));
Assert.That(seventh.ControlPoints[3].Type.Value == null); Assert.That(seventh.ControlPoints[3].Type == null);
Assert.That(seventh.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(410, 20))); Assert.That(seventh.ControlPoints[4].Position, Is.EqualTo(new Vector2(410, 20)));
Assert.That(seventh.ControlPoints[4].Type.Value == null); Assert.That(seventh.ControlPoints[4].Type == null);
} }
} }
} }

View File

@ -14,6 +14,7 @@ using osu.Framework.IO.Stores;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IO.Serialization; using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
@ -49,6 +50,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration)); Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
} }
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStabilityDoubleConvert(string name)
{
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
// run an extra convert. this is expected to be stable.
decodedAfterEncode.beatmap = convert(decodedAfterEncode.beatmap);
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
}
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStabilityWithNonLegacyControlPoints(string name)
{
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
// we are testing that the transfer of relevant data to hitobjects (from legacy control points) sticks through encode/decode.
// before the encode step, the legacy information is removed here.
decoded.beatmap.ControlPointInfo = removeLegacyControlPointTypes(decoded.beatmap.ControlPointInfo);
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
// in this process, we may lose some detail in the control points section.
// let's focus on only the hitobjects.
var originalHitObjects = decoded.beatmap.HitObjects.Serialize();
var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize();
Assert.That(newHitObjects, Is.EqualTo(originalHitObjects));
ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
{
// emulate non-legacy control points by cloning the non-legacy portion.
// the assertion is that the encoder can recreate this losslessly from hitobject data.
Assert.IsInstanceOf<LegacyControlPointInfo>(controlPointInfo);
var newControlPoints = new ControlPointInfo();
foreach (var point in controlPointInfo.AllControlPoints)
{
// completely ignore "legacy" types, which have been moved to HitObjects.
// even though these would mostly be ignored by the Add call, they will still be available in groups,
// which isn't what we want to be testing here.
if (point is SampleControlPoint)
continue;
newControlPoints.Add(point.Time, point.DeepClone());
}
return newControlPoints;
}
}
[Test] [Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError() public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{ {
@ -116,7 +174,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
private Stream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap) private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
{ {
var (beatmap, beatmapSkin) = fullBeatmap; var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream(); var stream = new MemoryStream();
@ -169,7 +227,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override Track GetBeatmapTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected override ISkin GetSkin() => throw new NotImplementedException(); protected internal override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException(); public override Stream GetStream(string storagePath) => throw new NotImplementedException();
} }

View File

@ -50,12 +50,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
var beatmap = decodeAsJson(normal); var beatmap = decodeAsJson(normal);
var beatmapInfo = beatmap.BeatmapInfo; var beatmapInfo = beatmap.BeatmapInfo;
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmapInfo.AudioLeadIn);
Assert.AreEqual(false, beatmapInfo.Countdown);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmapInfo.StackLeniency);
Assert.AreEqual(false, beatmapInfo.SpecialStyle); Assert.AreEqual(false, beatmapInfo.SpecialStyle);
Assert.IsTrue(beatmapInfo.RulesetID == 0); Assert.IsTrue(beatmapInfo.RulesetID == 0);
Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks);
Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset);
} }
[Test] [Test]

View File

@ -0,0 +1,173 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Beatmaps
{
[HeadlessTest]
public class TestSceneBeatmapDifficultyCache : OsuTestScene
{
public const double BASE_STARS = 5.55;
private BeatmapSetInfo importedSet;
[Resolved]
private BeatmapManager beatmaps { get; set; }
private TestBeatmapDifficultyCache difficultyCache;
private IBindable<StarDifficulty?> starDifficultyBindable;
[BackgroundDependencyLoader]
private void load(OsuGameBase osu)
{
importedSet = ImportBeatmapTest.LoadQuickOszIntoOsu(osu).Result;
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup difficulty cache", () =>
{
SelectedMods.Value = Array.Empty<Mod>();
Child = difficultyCache = new TestBeatmapDifficultyCache();
starDifficultyBindable = difficultyCache.GetBindableDifficulty(importedSet.Beatmaps.First());
});
AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value?.Stars == BASE_STARS);
}
[Test]
public void TestStarDifficultyChangesOnModSettings()
{
OsuModDoubleTime dt = null;
AddStep("set computation function", () => difficultyCache.ComputeDifficulty = lookup =>
{
var modRateAdjust = (ModRateAdjust)lookup.OrderedMods.SingleOrDefault(mod => mod is ModRateAdjust);
return new StarDifficulty(BASE_STARS + modRateAdjust?.SpeedChange.Value ?? 0, 0);
});
AddStep("change selected mod to DT", () => SelectedMods.Value = new[] { dt = new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
AddUntilStep($"star difficulty -> {BASE_STARS + 1.5}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.5);
AddStep("change DT speed to 1.25", () => dt.SpeedChange.Value = 1.25);
AddUntilStep($"star difficulty -> {BASE_STARS + 1.25}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.25);
AddStep("change selected mod to NC", () => SelectedMods.Value = new[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } });
AddUntilStep($"star difficulty -> {BASE_STARS + 1.75}", () => starDifficultyBindable.Value?.Stars == BASE_STARS + 1.75);
}
[Test]
public void TestStarDifficultyAdjustHashCodeConflict()
{
OsuModDifficultyAdjust difficultyAdjust = null;
AddStep("set computation function", () => difficultyCache.ComputeDifficulty = lookup =>
{
var modDifficultyAdjust = (ModDifficultyAdjust)lookup.OrderedMods.SingleOrDefault(mod => mod is ModDifficultyAdjust);
return new StarDifficulty(BASE_STARS * (modDifficultyAdjust?.OverallDifficulty.Value ?? 1), 0);
});
AddStep("change selected mod to DA", () => SelectedMods.Value = new[] { difficultyAdjust = new OsuModDifficultyAdjust() });
AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value?.Stars == BASE_STARS);
AddStep("change DA difficulty to 0.5", () => difficultyAdjust.OverallDifficulty.Value = 0.5f);
AddUntilStep($"star difficulty -> {BASE_STARS * 0.5f}", () => starDifficultyBindable.Value?.Stars == BASE_STARS / 2);
// hash code of 0 (the value) conflicts with the hash code of null (the initial/default value).
// it's important that the mod reference and its underlying bindable references stay the same to demonstrate this failure.
AddStep("change DA difficulty to 0", () => difficultyAdjust.OverallDifficulty.Value = 0);
AddUntilStep("star difficulty -> 0", () => starDifficultyBindable.Value?.Stars == 0);
}
[Test]
public void TestKeyEqualsWithDifferentModInstances()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
}
[Test]
public void TestKeyEqualsWithDifferentModOrder()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
}
[Test]
public void TestKeyDoesntEqualWithDifferentModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
Assert.That(key1, Is.Not.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode()));
}
[Test]
public void TestKeyEqualWithMatchingModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
}
[TestCase(1.3, DifficultyRating.Easy)]
[TestCase(1.993, DifficultyRating.Easy)]
[TestCase(1.998, DifficultyRating.Normal)]
[TestCase(2.4, DifficultyRating.Normal)]
[TestCase(2.693, DifficultyRating.Normal)]
[TestCase(2.698, DifficultyRating.Hard)]
[TestCase(3.5, DifficultyRating.Hard)]
[TestCase(3.993, DifficultyRating.Hard)]
[TestCase(3.997, DifficultyRating.Insane)]
[TestCase(5.0, DifficultyRating.Insane)]
[TestCase(5.292, DifficultyRating.Insane)]
[TestCase(5.297, DifficultyRating.Expert)]
[TestCase(6.2, DifficultyRating.Expert)]
[TestCase(6.493, DifficultyRating.Expert)]
[TestCase(6.498, DifficultyRating.ExpertPlus)]
[TestCase(8.3, DifficultyRating.ExpertPlus)]
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
{
var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating);
Assert.AreEqual(expectedBracket, actualBracket);
}
private class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{
public Func<DifficultyCacheLookup, StarDifficulty> ComputeDifficulty { get; set; }
protected override Task<StarDifficulty> ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default)
{
return Task.FromResult(ComputeDifficulty?.Invoke(lookup) ?? new StarDifficulty(BASE_STARS, 0));
}
}
}
}

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.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -19,6 +20,7 @@ namespace osu.Game.Tests.Chat
{ {
private ChannelManager channelManager; private ChannelManager channelManager;
private int currentMessageId; private int currentMessageId;
private List<Message> sentMessages;
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
@ -34,6 +36,7 @@ namespace osu.Game.Tests.Chat
AddStep("register request handling", () => AddStep("register request handling", () =>
{ {
currentMessageId = 0; currentMessageId = 0;
sentMessages = new List<Message>();
((DummyAPIAccess)API).HandleRequest = req => ((DummyAPIAccess)API).HandleRequest = req =>
{ {
@ -44,16 +47,11 @@ namespace osu.Game.Tests.Chat
return true; return true;
case PostMessageRequest postMessage: case PostMessageRequest postMessage:
postMessage.TriggerSuccess(new Message(++currentMessageId) handlePostMessageRequest(postMessage);
{ return true;
IsAction = postMessage.Message.IsAction,
ChannelId = postMessage.Message.ChannelId,
Content = postMessage.Message.Content,
Links = postMessage.Message.Links,
Timestamp = postMessage.Message.Timestamp,
Sender = postMessage.Message.Sender
});
case MarkChannelAsReadRequest markRead:
handleMarkChannelAsReadRequest(markRead);
return true; return true;
} }
@ -83,12 +81,65 @@ namespace osu.Game.Tests.Chat
AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to")); AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to"));
} }
[Test]
public void TestMarkAsReadIgnoringLocalMessages()
{
Channel channel = null;
AddStep("join channel and select it", () =>
{
channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public));
channelManager.CurrentChannel.Value = channel;
});
AddStep("post message", () => channelManager.PostMessage("Something interesting"));
AddStep("post /help command", () => channelManager.PostCommand("help", channel));
AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel));
AddStep("post /join command with no channel", () => channelManager.PostCommand("join", channel));
AddStep("post /join command with non-existent channel", () => channelManager.PostCommand("join i-dont-exist", channel));
AddStep("post non-existent command", () => channelManager.PostCommand("non-existent-cmd arg", channel));
AddStep("mark channel as read", () => channelManager.MarkChannelAsRead(channel));
AddAssert("channel's last read ID is set to the latest message", () => channel.LastReadId == sentMessages.Last().Id);
}
private void handlePostMessageRequest(PostMessageRequest request)
{
var message = new Message(++currentMessageId)
{
IsAction = request.Message.IsAction,
ChannelId = request.Message.ChannelId,
Content = request.Message.Content,
Links = request.Message.Links,
Timestamp = request.Message.Timestamp,
Sender = request.Message.Sender
};
sentMessages.Add(message);
request.TriggerSuccess(message);
}
private void handleMarkChannelAsReadRequest(MarkChannelAsReadRequest request)
{
// only accept messages that were sent through the API
if (sentMessages.Contains(request.Message))
{
request.TriggerSuccess();
}
else
{
request.TriggerFailure(new APIException("unknown message!", null));
}
}
private Channel createChannel(int id, ChannelType type) => new Channel(new User()) private Channel createChannel(int id, ChannelType type) => new Channel(new User())
{ {
Id = id, Id = id,
Name = $"Channel {id}", Name = $"Channel {id}",
Topic = $"Topic of channel {id} with type {type}", Topic = $"Topic of channel {id} with type {type}",
Type = type, Type = type,
LastMessageId = 0,
}; };
private class ChannelManagerContainer : CompositeDrawable private class ChannelManagerContainer : CompositeDrawable

View File

@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Collections.IO namespace osu.Game.Tests.Collections.IO
@ -127,7 +128,7 @@ namespace osu.Game.Tests.Collections.IO
[Test] [Test]
public async Task TestSaveAndReload() public async Task TestSaveAndReload()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload", bypassCleanup: true))
{ {
try try
{ {
@ -148,7 +149,7 @@ namespace osu.Game.Tests.Collections.IO
} }
} }
using (HeadlessGameHost host = new HeadlessGameHost("TestSaveAndReload")) using (HeadlessGameHost host = new TestRunHeadlessGameHost("TestSaveAndReload"))
{ {
try try
{ {

View File

@ -11,6 +11,7 @@ using osu.Framework.Platform;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using Realms; using Realms;
namespace osu.Game.Tests.Database namespace osu.Game.Tests.Database
@ -42,7 +43,7 @@ namespace osu.Game.Tests.Database
KeyBindingContainer testContainer = new TestKeyBindingContainer(); KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer); keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
Assert.That(queryCount(), Is.EqualTo(3)); Assert.That(queryCount(), Is.EqualTo(3));
@ -66,7 +67,7 @@ namespace osu.Game.Tests.Database
{ {
KeyBindingContainer testContainer = new TestKeyBindingContainer(); KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer); keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
using (var primaryUsage = realmContextFactory.GetForRead()) using (var primaryUsage = realmContextFactory.GetForRead())
{ {

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks; using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -30,7 +31,7 @@ namespace osu.Game.Tests.Editing.Checks
{ {
check = new CheckMutedObjects(); check = new CheckMutedObjects();
cpi = new ControlPointInfo(); cpi = new LegacyControlPointInfo();
cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular }); cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low }); cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted }); cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });

View File

@ -2,11 +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.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay namespace osu.Game.Tests.Gameplay
@ -121,6 +123,18 @@ namespace osu.Game.Tests.Gameplay
AddAssert("Drawable lifetime is restored", () => dho.LifetimeStart == 666 && dho.LifetimeEnd == 999); AddAssert("Drawable lifetime is restored", () => dho.LifetimeStart == 666 && dho.LifetimeEnd == 999);
} }
[Test]
public void TestStateChangeBeforeLoadComplete()
{
TestDrawableHitObject dho = null;
AddStep("Add DHO and apply result", () =>
{
Child = dho = new TestDrawableHitObject(new HitObject { StartTime = Time.Current });
dho.MissForcefully();
});
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
}
private class TestDrawableHitObject : DrawableHitObject private class TestDrawableHitObject : DrawableHitObject
{ {
public const double INITIAL_LIFETIME_OFFSET = 100; public const double INITIAL_LIFETIME_OFFSET = 100;
@ -141,6 +155,19 @@ namespace osu.Game.Tests.Gameplay
if (SetLifetimeStartOnApply) if (SetLifetimeStartOnApply)
LifetimeStart = LIFETIME_ON_APPLY; LifetimeStart = LIFETIME_ON_APPLY;
} }
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
protected override void UpdateHitStateTransforms(ArmedState state)
{
if (state != ArmedState.Miss)
{
base.UpdateHitStateTransforms(state);
return;
}
this.FadeOut(1000);
}
} }
private class TestLifetimeEntry : HitObjectLifetimeEntry private class TestLifetimeEntry : HitObjectLifetimeEntry

View File

@ -204,7 +204,7 @@ namespace osu.Game.Tests.Gameplay
this.resources = resources; this.resources = resources;
} }
protected override ISkin GetSkin() => new TestSkin("test-sample", resources); protected internal override ISkin GetSkin() => new TestSkin("test-sample", resources);
} }
private class TestDrawableStoryboardSample : DrawableStoryboardSample private class TestDrawableStoryboardSample : DrawableStoryboardSample

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