同步更新,调整资源工厂逻辑,优化EzPro皮肤逻辑

This commit is contained in:
LA
2025-07-19 20:47:44 +08:00
parent ba4e88a9c2
commit 5c2c937ca1
150 changed files with 3493 additions and 2037 deletions

View File

@@ -50,7 +50,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl> <RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes> <PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company> <Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright> <Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags> <PackageTags>osu game</PackageTags>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.704.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2025.718.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using Android.App; using Android.App;
using Android.Content.PM; using Android.Content.PM;
using Microsoft.Maui.Devices; using Microsoft.Maui.Devices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
@@ -21,58 +23,30 @@ namespace osu.Android
[Cached] [Cached]
private readonly OsuGameActivity gameActivity; private readonly OsuGameActivity gameActivity;
private readonly PackageInfo packageInfo;
public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth); public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
public OsuGameAndroid(OsuGameActivity activity) public OsuGameAndroid(OsuGameActivity activity)
: base(null) : base(null)
{ {
gameActivity = activity; gameActivity = activity;
packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull();
} }
public override Version AssemblyVersion public override string Version
{ {
get get
{ {
var packageInfo = Application.Context.ApplicationContext!.PackageManager!.GetPackageInfo(Application.Context.ApplicationContext.PackageName!, 0).AsNonNull(); if (!IsDeployedBuild)
return @"local " + (DebugUtils.IsDebugBuild ? @"debug" : @"release");
try return packageInfo.VersionName.AsNonNull();
{
// We store the osu! build number in the "VersionCode" field to better support google play releases.
// If we were to use the main build number, it would require a new submission each time (similar to TestFlight).
// In order to do this, we should split it up and pad the numbers to still ensure sequential increase over time.
//
// We also need to be aware that older SDK versions store this as a 32bit int.
//
// Basic conversion format (as done in Fastfile): 2020.606.0 -> 202006060
// https://stackoverflow.com/questions/52977079/android-sdk-28-versioncode-in-packageinfo-has-been-deprecated
string versionName;
if (OperatingSystem.IsAndroidVersionAtLeast(28))
{
versionName = packageInfo.LongVersionCode.ToString();
// ensure we only read the trailing portion of long (the part we are interested in).
versionName = versionName.Substring(versionName.Length - 9);
}
else
{
#pragma warning disable CS0618 // Type or member is obsolete
// this is required else older SDKs will report missing method exception.
versionName = packageInfo.VersionCode.ToString();
#pragma warning restore CS0618 // Type or member is obsolete
}
// undo play store version garbling (as mentioned above).
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
catch
{
}
return new Version(packageInfo.VersionName.AsNonNull());
} }
} }
public override Version AssemblyVersion => new Version(packageInfo.VersionName.AsNonNull().Split('-').First());
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();

View File

@@ -1,32 +1,32 @@
{ {
"solution": { "solution": {
"path": "osu.sln", "path": "osu.sln",
"projects": [ "projects": [
"..\\osu-resources\\osu.Game.Resources\\osu.Game.Resources.csproj", "..\\osu-framework\\osu.Framework.NativeLibs\\osu.Framework.NativeLibs.csproj",
"osu.Desktop\\osu.Desktop.csproj", "..\\osu-framework\\osu.Framework\\osu.Framework.csproj",
"osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj", "..\\osu-resources\\osu.Game.Resources\\osu.Game.Resources.csproj",
"osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.csproj", "osu.Desktop\\osu.Desktop.csproj",
"osu.Game.Rulesets.Catch\\osu.Game.Rulesets.Catch.csproj", "osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj",
"osu.Game.Rulesets.Mania.Tests\\osu.Game.Rulesets.Mania.Tests.csproj", "osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.csproj",
"osu.Game.Rulesets.Mania\\osu.Game.Rulesets.Mania.csproj", "osu.Game.Rulesets.Catch\\osu.Game.Rulesets.Catch.csproj",
"osu.Game.Rulesets.Osu.Tests\\osu.Game.Rulesets.Osu.Tests.csproj", "osu.Game.Rulesets.Mania.Tests\\osu.Game.Rulesets.Mania.Tests.csproj",
"osu.Game.Rulesets.Osu\\osu.Game.Rulesets.Osu.csproj", "osu.Game.Rulesets.Mania\\osu.Game.Rulesets.Mania.csproj",
"osu.Game.Rulesets.Taiko.Tests\\osu.Game.Rulesets.Taiko.Tests.csproj", "osu.Game.Rulesets.Osu.Tests\\osu.Game.Rulesets.Osu.Tests.csproj",
"osu.Game.Rulesets.Taiko\\osu.Game.Rulesets.Taiko.csproj", "osu.Game.Rulesets.Osu\\osu.Game.Rulesets.Osu.csproj",
"osu.Game.Tests\\osu.Game.Tests.csproj", "osu.Game.Rulesets.Taiko.Tests\\osu.Game.Rulesets.Taiko.Tests.csproj",
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj", "osu.Game.Rulesets.Taiko\\osu.Game.Rulesets.Taiko.csproj",
"osu.Game.Tournament\\osu.Game.Tournament.csproj", "osu.Game.Tests\\osu.Game.Tests.csproj",
"osu.Game\\osu.Game.csproj", "osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj", "osu.Game.Tournament\\osu.Game.Tournament.csproj",
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj", "osu.Game\\osu.Game.csproj",
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj", "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj",
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", "Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj", "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj", "Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj", "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj", "Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
"../osu-framework/osu.Framework/osu.Framework.csproj", "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
"../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj" "Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj"
] ]
} }
} }

View File

@@ -123,7 +123,7 @@ namespace osu.Desktop
public override bool RestartAppWhenExited() public override bool RestartAppWhenExited()
{ {
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget(); Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
return true; return true;
} }

View File

@@ -53,33 +53,44 @@ namespace osu.Desktop.Updater
return false; return false;
} }
IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon); try
Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
{ {
AllowVersionDowngrade = true IUpdateSource updateSource = new GithubSource(@"https://github.com/ppy/osu", null, ReleaseStream.Value == Game.Configuration.ReleaseStream.Tachyon);
}); Velopack.UpdateManager updateManager = new Velopack.UpdateManager(updateSource, new UpdateOptions
{
AllowVersionDowngrade = true
});
UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); UpdateInfo? update = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false);
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
{
log("Update check cancelled");
scheduleNextUpdateCheck();
return true;
}
if (update == null)
{
// No update is available.
log("No update found");
scheduleNextUpdateCheck();
return false;
}
// Download update in the background while notifying awaiters of the update being available.
log($"New update available: {update.TargetFullRelease.Version}");
downloadUpdate(updateManager, update, cancellationToken);
return true;
}
catch (Exception e)
{ {
log("Update check cancelled"); log($"Update check failed with error ({e.Message})");
// we shouldn't crash on a web failure. or any failure for the matter.
scheduleNextUpdateCheck(); scheduleNextUpdateCheck();
return true; return true;
} }
if (update == null)
{
// No update is available.
log("No update found");
scheduleNextUpdateCheck();
return false;
}
// Download update in the background while notifying awaiters of the update being available.
log($"New update available: {update.TargetFullRelease.Version}");
downloadUpdate(updateManager, update, cancellationToken);
return true;
} }
private void downloadUpdate(Velopack.UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () => private void downloadUpdate(Velopack.UpdateManager updateManager, UpdateInfo update, CancellationToken cancellationToken) => Task.Run(async () =>

View File

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

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
{ {
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset(); var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate }; var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate)); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
} }
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset(); var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty(); var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModHalfTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01)); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
} }
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset(); var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty(); var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new CatchModDoubleTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01)); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
} }

View File

@@ -33,6 +33,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Catch namespace osu.Game.Rulesets.Catch
@@ -265,9 +266,10 @@ namespace osu.Game.Rulesets.Catch
} }
/// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/> /// <seealso cref="CatchHitObject.ApplyDefaultsToSelf"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{ {
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN); double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
preempt /= rate; preempt /= rate;

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods namespace osu.Game.Rulesets.Catch.Mods
@@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModEasy : ModEasyWithExtraLives public class CatchModEasy : ModEasyWithExtraLives
{ {
public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!"; public override LocalisableString Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and extra lives!";
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
}
} }
} }

View File

@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
base.ApplyToDifficulty(difficulty); base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
} }

View File

@@ -5,7 +5,6 @@ using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
@@ -38,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests
new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } }, new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } },
new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } }, new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } }, new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } },
new object[] { LegacyMods.ScoreV2, new[] { typeof(ModScoreV2) } }, new object[] { LegacyMods.ScoreV2, new[] { typeof(ManiaModScoreV2) } },
}; };
[TestCaseSource(nameof(mania_mod_mapping))] [TestCaseSource(nameof(mania_mod_mapping))]

View File

@@ -9,7 +9,6 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@@ -521,14 +520,13 @@ namespace osu.Game.Rulesets.Mania.Tests
ScoreInfo = new ScoreInfo ScoreInfo = new ScoreInfo
{ {
Ruleset = CreateRuleset().RulesetInfo, Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ModScoreV2()] Mods = [new ManiaModScoreV2()]
} }
}; };
RunTest($@"SV2 single note @ OD{overallDifficulty}", beatmap, $@"SV2 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); RunTest($@"SV2 single note @ OD{overallDifficulty}", beatmap, $@"SV2 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
} }
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_test_cases))] [TestCaseSource(nameof(score_v1_non_convert_test_cases))]
public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) public void TestHitWindowTreatmentWithScoreV1NonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{ {
@@ -556,7 +554,6 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1 single note @ OD{overallDifficulty}", beatmap, $@"SV1 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); RunTest($@"SV1 single note @ OD{overallDifficulty}", beatmap, $@"SV1 {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
} }
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_convert_test_cases))] [TestCaseSource(nameof(score_v1_convert_test_cases))]
public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult) public void TestHitWindowTreatmentWithScoreV1Convert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{ {
@@ -585,7 +582,6 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); RunTest($@"SV1 convert single note @ OD{overallDifficulty}", beatmap, $@"SV1 convert {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
} }
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_hard_rock_test_cases))] [TestCaseSource(nameof(score_v1_non_convert_hard_rock_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndHardRockNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) public void TestHitWindowTreatmentWithScoreV1AndHardRockNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{ {
@@ -614,7 +610,6 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1+HR single note @ OD{overallDifficulty}", beatmap, $@"SV1+HR {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); RunTest($@"SV1+HR single note @ OD{overallDifficulty}", beatmap, $@"SV1+HR {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
} }
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_easy_test_cases))] [TestCaseSource(nameof(score_v1_non_convert_easy_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndEasyNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) public void TestHitWindowTreatmentWithScoreV1AndEasyNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{ {
@@ -643,7 +638,6 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1+EZ single note @ OD{overallDifficulty}", beatmap, $@"SV1+EZ {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); RunTest($@"SV1+EZ single note @ OD{overallDifficulty}", beatmap, $@"SV1+EZ {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
} }
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_double_time_test_cases))] [TestCaseSource(nameof(score_v1_non_convert_double_time_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndDoubleTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) public void TestHitWindowTreatmentWithScoreV1AndDoubleTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{ {
@@ -672,7 +666,6 @@ namespace osu.Game.Rulesets.Mania.Tests
RunTest($@"SV1+DT single note @ OD{overallDifficulty}", beatmap, $@"SV1+DT {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]); RunTest($@"SV1+DT single note @ OD{overallDifficulty}", beatmap, $@"SV1+DT {hitOffset}ms @ OD{overallDifficulty} = {expectedResult}", score, [expectedResult]);
} }
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_non_convert_half_time_test_cases))] [TestCaseSource(nameof(score_v1_non_convert_half_time_test_cases))]
public void TestHitWindowTreatmentWithScoreV1AndHalfTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult) public void TestHitWindowTreatmentWithScoreV1AndHalfTimeNonConvert(float overallDifficulty, double hitOffset, HitResult expectedResult)
{ {

View File

@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Mania.LAsEZMania
float width = (float)columnWidth.Value; float width = (float)columnWidth.Value;
int index = KeyFlow.IndexOf(counter); int index = KeyFlow.IndexOf(counter);
if (ezSkinConfig.GetColumnType(StageDefinition.Columns, index) == "S1") if (ezSkinConfig.GetColumnType(StageDefinition.Columns, index) == "S")
width *= (float)specialFactor.Value; width *= (float)specialFactor.Value;
counter.Width = width; counter.Width = width;

View File

@@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Mania
yield return new ManiaModMirror(); yield return new ManiaModMirror();
if (mods.HasFlag(LegacyMods.ScoreV2)) if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2(); yield return new ManiaModScoreV2();
} }
public override LegacyMods ConvertToLegacyMods(Mod[] mods) public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@@ -356,7 +356,7 @@ namespace osu.Game.Rulesets.Mania
case ModType.System: case ModType.System:
return new Mod[] return new Mod[]
{ {
new ModScoreV2(), new ManiaModScoreV2(),
}; };
default: default:
@@ -490,6 +490,32 @@ namespace osu.Game.Rulesets.Mania
return statistics.ToArray(); return statistics.ToArray();
} }
/// <seealso cref="ManiaHitWindows"/>
public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
// notably, in mania, hit windows are designed to be independent of track playback rate (see `ManiaHitWindows.SpeedMultiplier`).
// *however*, to not make matters *too* simple, mania Hard Rock and Easy differ from all other rulesets
// in that they apply multipliers *to hit window durations directly* rather than to the Overall Difficulty attribute itself.
// because the duration of hit window durations as a function of OD is not a linear function,
// this means that multiplying the OD is *not* the same thing as multiplying the hit window duration.
// in fact, the second operation is *much* harsher and will produce values much farther outside of normal operating range
// (even negative in the case of Easy).
// stable handles this wrong on song select and just assumes that it can handle mania EZ / HR the same way as all other rulesets.
double perfectHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, ManiaHitWindows.PERFECT_WINDOW_RANGE);
if (mods.Any(m => m is ManiaModHardRock))
perfectHitWindow /= ManiaModHardRock.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
else if (mods.Any(m => m is ManiaModEasy))
perfectHitWindow /= ManiaModEasy.HIT_WINDOW_DIFFICULTY_MULTIPLIER;
adjustedDifficulty.OverallDifficulty = (float)IBeatmapDifficultyInfo.InverseDifficultyRange(perfectHitWindow, ManiaHitWindows.PERFECT_WINDOW_RANGE);
return adjustedDifficulty;
}
public override IRulesetFilterCriteria CreateRulesetFilterCriteria() public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
{ {
return new ManiaFilterCriteria(); return new ManiaFilterCriteria();

View File

@@ -2,12 +2,10 @@
// 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.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
@@ -17,29 +15,21 @@ namespace osu.Game.Rulesets.Mania.Mods
/// <remarks> /// <remarks>
/// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same. /// Historically, in osu!mania, hit windows are expected to adjust relative to the gameplay rate such that the real-world hit window remains the same.
/// </remarks> /// </remarks>
public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject public interface IManiaRateAdjustmentMod : IApplicableToHitObject
{ {
BindableNumber<double> SpeedChange { get; } BindableNumber<double> SpeedChange { get; }
HitWindows HitWindows { get; set; }
void IApplicableToDifficulty.ApplyToDifficulty(BeatmapDifficulty difficulty)
{
HitWindows = new ManiaHitWindows(SpeedChange.Value);
HitWindows.SetDifficulty(difficulty.OverallDifficulty);
}
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject) void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{ {
switch (hitObject) switch (hitObject)
{ {
case Note: case Note:
hitObject.HitWindows = HitWindows; ((ManiaHitWindows)hitObject.HitWindows).SpeedMultiplier = SpeedChange.Value;
break; break;
case HoldNote hold: case HoldNote hold:
hold.Head.HitWindows = HitWindows; ((ManiaHitWindows)hold.Head.HitWindows).SpeedMultiplier = SpeedChange.Value;
hold.Tail.HitWindows = HitWindows; ((ManiaHitWindows)hold.Tail.HitWindows).SpeedMultiplier = SpeedChange.Value;
break; break;
} }
} }

View File

@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
public override string Acronym => "SB"; public override string Acronym => "SB";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override LocalisableString Description => "LaMod: Hold the keys. Adjust the gaps."; public override LocalisableString Description => "LaMod: Full LN 面海, 可调面缝";
public override ModType Type => ModType.CustomMod; public override ModType Type => ModType.CustomMod;

View File

@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => Name; public override string Acronym => Name;
public abstract int KeyCount { get; } public abstract int KeyCount { get; }
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier public override double ScoreMultiplier => 0.9;
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => UsesDefaultConfiguration;
public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter) public void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)

View File

@@ -1,11 +1,41 @@
// 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.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModClassic : ModClassic public class ManiaModClassic : ModClassic, IApplicableToBeatmap
{ {
public void ApplyToBeatmap(IBeatmap beatmap)
{
bool isConvert = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
foreach (var ho in beatmap.HitObjects)
{
switch (ho)
{
case Note note:
{
var hitWindows = (ManiaHitWindows)note.HitWindows;
hitWindows.IsConvert = isConvert;
hitWindows.ClassicModActive = true;
break;
}
case HoldNote hold:
{
var headWindows = (ManiaHitWindows)hold.Head.HitWindows;
var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows;
headWindows.IsConvert = tailWindows.IsConvert = isConvert;
headWindows.ClassicModActive = tailWindows.ClassicModActive = true;
break;
}
}
}
}
} }
} }

View File

@@ -1,14 +1,11 @@
// 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.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod public class ManiaModDaycore : ModDaycore, IManiaRateAdjustmentMod
{ {
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
} }
} }

View File

@@ -1,16 +1,12 @@
// 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.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod public class ManiaModDoubleTime : ModDoubleTime, IManiaRateAdjustmentMod
{ {
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always // For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map harder and is more of a personal preference. // make the map harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency. // In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.

View File

@@ -2,12 +2,32 @@
// 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.Localisation; using osu.Framework.Localisation;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModEasy : ModEasyWithExtraLives public class ManiaModEasy : ModEasyWithExtraLives, IApplicableToHitObject
{ {
public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!"; public override LocalisableString Description => @"More forgiving HP drain, less accuracy required, and extra lives!";
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1 / 1.4;
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
}
}
} }
} }

View File

@@ -1,14 +1,11 @@
// 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.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod public class ManiaModHalfTime : ModHalfTime, IManiaRateAdjustmentMod
{ {
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
} }
} }

View File

@@ -1,13 +1,33 @@
// 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.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModHardRock : ModHardRock public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
{ {
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool Ranked => false; public override bool Ranked => false;
public const double HIT_WINDOW_DIFFICULTY_MULTIPLIER = 1.4;
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = HIT_WINDOW_DIFFICULTY_MULTIPLIER;
break;
}
}
} }
} }

View File

@@ -2,16 +2,12 @@
// 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.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods namespace osu.Game.Rulesets.Mania.Mods
{ {
public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod public class ManiaModNightcore : ModNightcore<ManiaHitObject>, IManiaRateAdjustmentMod
{ {
public HitWindows HitWindows { get; set; } = new ManiaHitWindows();
// For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always // For now, all rate-increasing mods should be given a 1x multiplier in mania because it doesn't always
// make the map any harder and is more of a personal preference. // make the map any harder and is more of a personal preference.
// In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency. // In the future, we can consider adjusting this by experimenting with not applying the hitwindow leniency.

View File

@@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModScoreV2 : ModScoreV2, IApplicableToBeatmap
{
public void ApplyToBeatmap(IBeatmap beatmap)
{
foreach (var ho in beatmap.HitObjects)
{
switch (ho)
{
case Note note:
{
var hitWindows = (ManiaHitWindows)note.HitWindows;
hitWindows.ScoreV2Active = true;
break;
}
case HoldNote hold:
{
var headWindows = (ManiaHitWindows)hold.Head.HitWindows;
var tailWindows = (ManiaHitWindows)hold.Tail.HitWindows;
headWindows.ScoreV2Active = tailWindows.ScoreV2Active = true;
break;
}
}
}
}
}
}

View File

@@ -9,14 +9,91 @@ namespace osu.Game.Rulesets.Mania.Scoring
{ {
public class ManiaHitWindows : HitWindows public class ManiaHitWindows : HitWindows
{ {
private static readonly DifficultyRange perfect_window_range = new DifficultyRange(22.4D, 19.4D, 13.9D); public static readonly DifficultyRange PERFECT_WINDOW_RANGE = new DifficultyRange(22.4D, 19.4D, 13.9D);
private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34); private static readonly DifficultyRange great_window_range = new DifficultyRange(64, 49, 34);
private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67); private static readonly DifficultyRange good_window_range = new DifficultyRange(97, 82, 67);
private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97); private static readonly DifficultyRange ok_window_range = new DifficultyRange(127, 112, 97);
private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121); private static readonly DifficultyRange meh_window_range = new DifficultyRange(151, 136, 121);
private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158); private static readonly DifficultyRange miss_window_range = new DifficultyRange(188, 173, 158);
private readonly double multiplier; private double speedMultiplier = 1;
/// <summary>
/// Multiplier used to compensate for the playback speed of the track speeding up or slowing down.
/// The goal of this multiplier is to keep hit windows independent of track speed.
/// <list type="bullet">
/// <item>When the track speed is above 1, the hit window ranges are multiplied by <see cref="SpeedMultiplier"/>, because the time elapses faster.</item>
/// <item>When the track speed is below 1, the hit window ranges are also multiplied by <see cref="SpeedMultiplier"/>, because the time elapses slower.</item>
/// </list>
/// </summary>
public double SpeedMultiplier
{
get => speedMultiplier;
set
{
speedMultiplier = value;
updateWindows();
}
}
private double difficultyMultiplier = 1;
/// <summary>
/// Multiplier used to make the gameplay more or less difficult.
/// <list type="bullet">
/// <item>When the <see cref="DifficultyMultiplier"/> is above 1, the hit windows decrease to make the gameplay harder.</item>
/// <item>When the <see cref="DifficultyMultiplier"/> is below 1, the hit windows increase to make the gameplay easier.</item>
/// </list>
/// </summary>
public double DifficultyMultiplier
{
get => difficultyMultiplier;
set
{
difficultyMultiplier = value;
updateWindows();
}
}
private double totalMultiplier => speedMultiplier / difficultyMultiplier;
private double overallDifficulty;
private bool classicModActive;
public bool ClassicModActive
{
get => classicModActive;
set
{
classicModActive = value;
updateWindows();
}
}
private bool scoreV2Active;
public bool ScoreV2Active
{
get => scoreV2Active;
set
{
scoreV2Active = value;
updateWindows();
}
}
private bool isConvert;
public bool IsConvert
{
get => isConvert;
set
{
isConvert = value;
updateWindows();
}
}
private double perfect; private double perfect;
private double great; private double great;
@@ -25,16 +102,6 @@ namespace osu.Game.Rulesets.Mania.Scoring
private double meh; private double meh;
private double miss; private double miss;
public ManiaHitWindows()
: this(1)
{
}
public ManiaHitWindows(double multiplier)
{
this.multiplier = multiplier;
}
public override bool IsHitResultAllowed(HitResult result) public override bool IsHitResultAllowed(HitResult result)
{ {
switch (result) switch (result)
@@ -55,12 +122,44 @@ namespace osu.Game.Rulesets.Mania.Scoring
public override void SetDifficulty(double difficulty) public override void SetDifficulty(double difficulty)
{ {
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) * multiplier) + 0.5; overallDifficulty = difficulty;
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range) * multiplier) + 0.5; updateWindows();
good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, good_window_range) * multiplier) + 0.5; }
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, ok_window_range) * multiplier) + 0.5;
meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range) * multiplier) + 0.5; private void updateWindows()
miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range) * multiplier) + 0.5; {
if (ClassicModActive && !ScoreV2Active)
{
if (IsConvert)
{
perfect = Math.Floor(16 * totalMultiplier) + 0.5;
great = Math.Floor((Math.Round(overallDifficulty) > 4 ? 34 : 47) * totalMultiplier) + 0.5;
good = Math.Floor((Math.Round(overallDifficulty) > 4 ? 67 : 77) * totalMultiplier) + 0.5;
ok = Math.Floor(97 * totalMultiplier) + 0.5;
meh = Math.Floor(121 * totalMultiplier) + 0.5;
miss = Math.Floor(158 * totalMultiplier) + 0.5;
}
else
{
double invertedOd = Math.Clamp(10 - overallDifficulty, 0, 10);
perfect = Math.Floor(16 * totalMultiplier) + 0.5;
great = Math.Floor((34 + 3 * invertedOd) * totalMultiplier) + 0.5;
good = Math.Floor((67 + 3 * invertedOd) * totalMultiplier) + 0.5;
ok = Math.Floor((97 + 3 * invertedOd) * totalMultiplier) + 0.5;
meh = Math.Floor((121 + 3 * invertedOd) * totalMultiplier) + 0.5;
miss = Math.Floor((158 + 3 * invertedOd) * totalMultiplier) + 0.5;
}
}
else
{
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, PERFECT_WINDOW_RANGE) * totalMultiplier) + 0.5;
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, great_window_range) * totalMultiplier) + 0.5;
good = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, good_window_range) * totalMultiplier) + 0.5;
ok = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, ok_window_range) * totalMultiplier) + 0.5;
meh = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, meh_window_range) * totalMultiplier) + 0.5;
miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(overallDifficulty, miss_window_range) * totalMultiplier) + 0.5;
}
} }
public override double WindowFor(HitResult result) public override double WindowFor(HitResult result)

View File

@@ -2,44 +2,29 @@
// 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.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.LAsEzExtensions; using osu.Game.Screens.LAsEzExtensions;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
public partial class EzHitExplosion : CompositeDrawable, IHitExplosion public partial class EzHitExplosion : EzNoteBase, IHitExplosion
{ {
private TextureAnimation? animation; protected override bool BoolUpdateColor => false;
private TextureAnimation? animationP;
private Container container = null!;
private IBindable<double> noteHeightBindable = new Bindable<double>();
private IBindable<double> columnWidthBindable = new Bindable<double>();
private IBindable<double> specialFactorBindable = new Bindable<double>();
// public override bool RemoveWhenNotAlive => true; // public override bool RemoveWhenNotAlive => true;
[Resolved]
private Column column { get; set; } = null!;
[Resolved]
private StageDefinition stageDefinition { get; set; } = null!;
[Resolved]
private EzSkinSettingsManager ezSkinConfig { get; set; } = null!;
[Resolved] [Resolved]
private EzLocalTextureFactory factory { get; set; } = null!; private EzLocalTextureFactory factory { get; set; } = null!;
private TextureAnimation? primaryAnimation;
private TextureAnimation? goodAnimation;
private bool animationsCreated;
public EzHitExplosion() public EzHitExplosion()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@@ -51,78 +36,54 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
Anchor = Anchor.BottomCentre; Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre; Origin = Anchor.BottomCentre;
OnDrawableChanged();
noteHeightBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight);
columnWidthBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.ColumnWidth);
specialFactorBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.SpecialFactor);
noteHeightBindable.BindValueChanged(_ => updateY(), true);
columnWidthBindable.BindValueChanged(_ => updateY(), true);
specialFactorBindable.BindValueChanged(_ => updateY(), true);
} }
protected override void LoadComplete() protected override void OnDrawableChanged()
{
base.LoadComplete();
onSkinChanged();
factory.OnNoteChanged += onSkinChanged;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing) { factory.OnNoteChanged -= onSkinChanged; }
}
private void loadAnimation()
{ {
ClearInternal(); ClearInternal();
primaryAnimation = null;
goodAnimation = null;
animation = factory.CreateAnimation("noteflare"); primaryAnimation = factory.CreateAnimation("noteflare");
animationP = factory.CreateAnimation("noteflaregood"); goodAnimation = factory.CreateAnimation("noteflaregood");
container = new Container if (primaryAnimation?.FrameCount > 0)
AddInternal(primaryAnimation);
if (goodAnimation?.FrameCount > 0)
{ {
Anchor = Anchor.BottomCentre, goodAnimation.Alpha = 0;
Origin = Anchor.BottomCentre, AddInternal(goodAnimation);
RelativeSizeAxes = Axes.None, }
Children = new Drawable[]
{ animation },
};
AddInternal(container); animationsCreated = true;
updateY();
} }
private void updateY() protected override void UpdateSize()
{ {
bool isSpecialColumn = ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) == "S1"; base.UpdateSize();
double columnWidth = columnWidthBindable.Value * (isSpecialColumn ? specialFactorBindable.Value : 1); float moveY = NoteSize.Value.Y / 2;
bool isSquare = factory.IsSquareNote("whitenote");
float aspectRatio = factory.GetRatio("whitenote");
float moveY = isSquare
? (float)columnWidth / 2 * aspectRatio
: (float)noteHeightBindable.Value / 2 * aspectRatio;
// baseYPosition = LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION - (float)hitPosition.Value - moveY; // baseYPosition = LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION - (float)hitPosition.Value - moveY;
Position = new Vector2(0, -moveY); Position = new Vector2(0, -moveY);
} }
private void onSkinChanged()
{
loadAnimation();
}
public void Animate(JudgementResult result) public void Animate(JudgementResult result)
{ {
loadAnimation(); if (!animationsCreated) OnDrawableChanged();
if (result.Type > HitResult.Great) if (primaryAnimation?.FrameCount > 0)
{ {
if (animationP != null) container.Add(animationP); primaryAnimation.Alpha = 1;
primaryAnimation.GotoFrame(0);
primaryAnimation.Restart();
}
if (result.Type >= HitResult.Great && goodAnimation?.FrameCount > 0)
{
goodAnimation.Alpha = 1;
goodAnimation.GotoFrame(0);
goodAnimation.Restart();
} }
} }
} }

View File

@@ -11,10 +11,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
internal partial class EzHitTarget : EzNote internal partial class EzHitTarget : EzNote
{ {
// private IBindable<double> hitPosition = new Bindable<double>(); protected override bool BoolUpdateColor => false;
protected override bool ShowSeparators => false;
protected override bool UseColorization => false; protected override bool UseColorization => false;
protected override bool ShowSeparators => false;
protected override string ColorPrefix => "white"; protected override string ColorPrefix => "white";
[Resolved] [Resolved]
@@ -23,9 +23,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
[Resolved] [Resolved]
private IGameplayClock gameplayClock { get; set; } = null!; private IGameplayClock gameplayClock { get; set; } = null!;
// [Resolved]
// private EzSkinSettingsManager ezSkinConfig { get; set; } = null!;
public EzHitTarget() public EzHitTarget()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@@ -38,11 +35,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
Anchor = Anchor.BottomCentre; Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre; Origin = Anchor.BottomCentre;
// hitPosition = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
// hitPosition.BindValueChanged(_ => updateY(), true);
} }
// private float baseYPosition = 0f;
private double beatInterval; private double beatInterval;
protected override void LoadComplete() protected override void LoadComplete()
@@ -61,12 +55,5 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
double smoothValue = 0.3 * Math.Sin(progress * 2 * Math.PI); double smoothValue = 0.3 * Math.Sin(progress * 2 * Math.PI);
Y = (float)(smoothValue * 6); Y = (float)(smoothValue * 6);
} }
//DrawableManiaRuleset中关联设置后此处不必设置
// private void updateY()
// {
// baseYPosition = LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION - (float)hitPosition.Value;
// Position = new Vector2(0, baseYPosition);
// }
} }
} }

View File

@@ -2,205 +2,79 @@
// 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.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Screens;
using osu.Game.Screens.LAsEzExtensions;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
public partial class EzHoldNoteHead : CompositeDrawable public partial class EzHoldNoteHead : EzNoteBase
{ {
private Bindable<bool> enabledColor = null!; protected override bool ShowSeparators => true;
private Bindable<double> nonSquareNoteHeight = null!;
private TextureAnimation animation = null!;
private Container container = null!;
private EzNoteSideLine? noteSeparatorsL;
private EzNoteSideLine? noteSeparatorsR;
[Resolved]
private Column column { get; set; } = null!;
[Resolved]
private StageDefinition stageDefinition { get; set; } = null!;
[Resolved]
private EzLocalTextureFactory factory { get; set; } = null!;
[Resolved]
private EzSkinSettingsManager ezSkinConfig { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
FillMode = FillMode.Fill; FillMode = FillMode.Fill;
nonSquareNoteHeight = ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight);
enabledColor = ezSkinConfig.GetBindable<bool>(EzSkinSetting.ColorSettingsEnabled);
OnSkinChanged();
} }
protected override void LoadComplete() protected override void OnDrawableChanged()
{ {
base.LoadComplete(); string newComponentName = $"{ColorPrefix}longnote/head";
nonSquareNoteHeight.BindValueChanged(_ => updateSizes(), true); var animation = Factory.CreateAnimation(newComponentName);
ezSkinConfig.OnSettingsChanged += OnConfigChanged;
factory.OnNoteChanged += OnSkinChanged;
}
protected override void Dispose(bool isDisposing) if (animation is TextureAnimation textureAnimation && textureAnimation.FrameCount == 0)
{
base.Dispose(isDisposing);
if (isDisposing)
{
ezSkinConfig.OnSettingsChanged -= OnConfigChanged;
factory.OnNoteChanged -= OnSkinChanged;
}
}
protected virtual Color4 NoteColor
{
get
{
int keyMode = stageDefinition.Columns;
int columnIndex = column.Index;
return ezSkinConfig.GetColumnColor(keyMode, columnIndex);
}
}
protected virtual string ColorPrefix
{
get
{
if (enabledColor.Value)
return "white";
if (ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) == "S1")
return "green";
int logicalIndex = 0;
for (int i = 0; i < column.Index; i++)
{
if (ezSkinConfig.GetColumnType(stageDefinition.Columns, i) != "S1")
logicalIndex++;
}
return logicalIndex % 2 == 0 ? "white" : "blue";
}
}
protected virtual string ComponentName => $"{ColorPrefix}longnote/head";
private void loadAnimation()
{
ClearInternal();
animation = factory.CreateAnimation(ComponentName);
container = new Container
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Masking = true,
};
noteSeparatorsL = new EzNoteSideLine
{
RelativeSizeAxes = Axes.X,
FillMode = FillMode.Fill,
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
};
noteSeparatorsR = new EzNoteSideLine
{
RelativeSizeAxes = Axes.X,
FillMode = FillMode.Fill,
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre,
// Rotation = 180,
};
AddInternal(new Container
{
RelativeSizeAxes = Axes.X,
FillMode = FillMode.Stretch,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
noteSeparatorsL,
noteSeparatorsR
}
});
if (animation.FrameCount == 0)
{ {
animation.Dispose(); animation.Dispose();
animation = factory.CreateAnimation($"{ColorPrefix}note"); animation = Factory.CreateAnimation($"{ColorPrefix}note");
var x = new Container
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Child = animation,
};
if (x.Child is TextureAnimation)
container.Child = x;
OnConfigChanged(); if (animation is TextureAnimation newTexture && newTexture.FrameCount == 0)
AddInternal(container); {
animation.Dispose();
return;
}
if (MainContainer != null)
{
MainContainer.Clear();
MainContainer.RelativeSizeAxes = Axes.X;
MainContainer.Anchor = Anchor.BottomCentre;
MainContainer.Origin = Anchor.BottomCentre;
MainContainer.Masking = true;
MainContainer.Child = new Container
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Child = animation,
};
}
} }
else else
{ {
OnConfigChanged(); if (MainContainer != null)
AddInternal(animation); {
MainContainer.Clear();
MainContainer.Child = animation;
}
} }
OnConfigChanged(); Schedule(UpdateSize);
} }
private void updateSizes() protected override void UpdateSize()
{ {
bool isSquare = factory.IsSquareNote("whitenote"); base.UpdateSize();
float noteHeight = isSquare float v = NoteSize.Value.Y;
? DrawWidth Height = v;
: (float)(ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight).Value);
Height = noteHeight; if (MainContainer?.Children.Count > 0 && MainContainer.Child is Container c)
if (container.Children.Count > 0 && container.Child is Container c)
{ {
container.Height = noteHeight / 2; MainContainer.Height = v / 2;
c.Height = noteHeight; c.Height = v;
} }
} }
private void OnConfigChanged()
{
var noteColor = Color4.White;
if (enabledColor.Value)
noteColor = NoteColor;
animation.Colour = noteColor;
container.Colour = noteColor;
noteSeparatorsL?.UpdateGlowEffect(noteColor);
noteSeparatorsR?.UpdateGlowEffect(noteColor);
Schedule(() =>
{
updateSizes();
Invalidate();
});
}
private void OnSkinChanged() => loadAnimation();
} }
} }

View File

@@ -6,39 +6,20 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.LAsEzExtensions; using osu.Game.Screens.LAsEzExtensions;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
public partial class EzHoldNoteHittingLayer : CompositeDrawable public partial class EzHoldNoteHittingLayer : EzNoteBase
{ {
protected override bool BoolUpdateColor => false;
public readonly Bindable<bool> IsHitting = new Bindable<bool>(); public readonly Bindable<bool> IsHitting = new Bindable<bool>();
private TextureAnimation? animation; private TextureAnimation? animation;
private IBindable<double> noteHeightBindable = new Bindable<double>();
private IBindable<double> columnWidthBindable = new Bindable<double>();
private IBindable<double> specialFactorBindable = new Bindable<double>();
public IBindable<double> HitPosition { get; set; } = new Bindable<double>(); public IBindable<double> HitPosition { get; set; } = new Bindable<double>();
// private IBindable<double> hitPosition = new Bindable<double>();
// private float baseYPosition;
[Resolved]
private Column column { get; set; } = null!;
[Resolved]
private StageDefinition stageDefinition { get; set; } = null!;
[Resolved]
private EzSkinSettingsManager ezSkinConfig { get; set; } = null!;
[Resolved] [Resolved]
private EzLocalTextureFactory factory { get; set; } = null!; private EzLocalTextureFactory factory { get; set; } = null!;
@@ -46,31 +27,20 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
Anchor = Anchor.BottomCentre; Anchor = Anchor.BottomCentre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
// AutoSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive; Blending = BlendingParameters.Additive;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
noteHeightBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight); HitPosition = EZSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
columnWidthBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.ColumnWidth); HitPosition.BindValueChanged(_ => UpdateSize(), true);
specialFactorBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.SpecialFactor);
HitPosition = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
noteHeightBindable.BindValueChanged(_ => UpdateLNsLight(), true);
columnWidthBindable.BindValueChanged(_ => UpdateLNsLight(), true);
specialFactorBindable.BindValueChanged(_ => UpdateLNsLight(), true);
// hitPosition.BindValueChanged(_ => updateY(), true);
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
onSkinChanged();
factory.OnNoteChanged += onSkinChanged;
HitPosition.BindValueChanged(pos => Y =
LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION - (float)pos.NewValue, true);
IsHitting.BindValueChanged(hitting => IsHitting.BindValueChanged(hitting =>
{ {
ClearTransforms(); ClearTransforms();
@@ -89,62 +59,48 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
}, true); }, true);
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
factory.OnNoteChanged -= onSkinChanged;
}
}
public void Recycle() public void Recycle()
{ {
ClearTransforms(); ClearTransforms();
Alpha = 0; Alpha = 0;
} }
private void loadAnimation() protected override void OnDrawableChanged()
{ {
ClearInternal(); string[] componentsToTry = { "longnoteflare", "noteflaregood", "noteflare" };
animation = factory.CreateAnimation("longnoteflare");
if (animation.FrameCount == 0) foreach (string component in componentsToTry)
{ {
animation.Dispose(); animation = factory.CreateAnimation(component);
animation = factory.CreateAnimation("noteflaregood");
if (animation.FrameCount == 0) if (animation.FrameCount > 0)
{ {
animation.Dispose(); animation.Loop = true;
animation = factory.CreateAnimation("noteflare"); break;
} }
} }
animation.Loop = true; if (animation is TextureAnimation textureAnimation && textureAnimation.FrameCount == 0)
AddInternal(animation); {
UpdateLNsLight(); animation.Dispose();
UpdateColor();
return;
}
if (MainContainer != null)
{
MainContainer.Clear();
if (animation != null) MainContainer.Child = animation;
}
Schedule(UpdateSize);
} }
public void UpdateLNsLight() protected override void UpdateSize()
{ {
bool isSpecialColumn = ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) == "S1"; base.UpdateSize();
double columnWidth = columnWidthBindable.Value * (isSpecialColumn ? specialFactorBindable.Value : 1); float v = NoteSize.Value.Y / 2;
Position = new Vector2(0, -(float)HitPosition.Value - v);
bool isSquare = factory.IsSquareNote("whitenote");
float aspectRatio = factory.GetRatio("whitenote");
float moveY = isSquare
? (float)columnWidth / 2 * aspectRatio
: (float)noteHeightBindable.Value * aspectRatio;
Position = new Vector2(0, moveY);
}
private void onSkinChanged()
{
loadAnimation();
} }
} }
} }

View File

@@ -5,52 +5,31 @@ using System;
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.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.LAsEzExtensions;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
public partial class EzHoldNoteMiddle : CompositeDrawable, IHoldNoteBody public partial class EzHoldNoteMiddle : EzNoteBase, IHoldNoteBody
{ {
private readonly IBindable<bool> isHitting = new Bindable<bool>(); private readonly IBindable<bool> isHitting = new Bindable<bool>();
private DrawableHoldNote holdNote = null!; private Container? topContainer;
private Container? bodyContainer;
private Container? bodyScaleContainer;
private Container? bodyInnerContainer;
private TextureAnimation? middleAnimation;
private TextureAnimation? tailAnimation;
private Container topContainer = null!;
private Container middleContainer = null!;
private Container middleScaleContainer = null!;
private Container middleInnerContainer = null!;
private IBindable<double> noteHeight = new Bindable<double>();
private IBindable<double> hitPosition = new Bindable<double>(); private IBindable<double> hitPosition = new Bindable<double>();
private Bindable<bool> enabledColor = null!; private EzHoldNoteHittingLayer? hittingLayer;
private Drawable? container;
private Drawable? lightContainer;
private EzHoldNoteHittingLayer hittingLayer = null!;
[Resolved] [Resolved]
private Column column { get; set; } = null!; private Column column { get; set; } = null!;
[Resolved]
private StageDefinition stageDefinition { get; set; } = null!;
[Resolved]
private EzSkinSettingsManager ezSkinConfig { get; set; } = null!;
[Resolved]
private EzLocalTextureFactory factory { get; set; } = null!;
public EzHoldNoteMiddle() public EzHoldNoteMiddle()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@@ -64,148 +43,77 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(DrawableHitObject drawableObject) private void load(DrawableHitObject drawableObject)
{ {
holdNote = (DrawableHoldNote)drawableObject; var holdNote = (DrawableHoldNote)drawableObject;
isHitting.BindTo(holdNote.IsHolding); isHitting.BindTo(holdNote.IsHolding);
noteHeight = ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight); hitPosition = EZSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
hitPosition = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
enabledColor = ezSkinConfig.GetBindable<bool>(EzSkinSetting.ColorSettingsEnabled);
}
public void Recycle()
{
ClearTransforms();
hittingLayer.Recycle();
}
protected override void Update()
{
base.Update();
updateSizes();
}
protected override void LoadComplete()
{
base.LoadComplete();
OnSkinChanged();
noteHeight.BindValueChanged(_ => OnSettingsChanged(), true);
factory.OnNoteChanged += OnSkinChanged;
ezSkinConfig.OnSettingsChanged += OnSettingsChanged;
isHitting.BindValueChanged(onIsHittingChanged, true); isHitting.BindValueChanged(onIsHittingChanged, true);
OnSkinChanged();
} }
protected override void Dispose(bool isDisposing) // protected override void Update()
{ // {
base.Dispose(isDisposing); // base.Update();
// }
if (isDisposing)
{
factory.OnNoteChanged -= OnSkinChanged;
ezSkinConfig.OnSettingsChanged -= OnSettingsChanged;
lightContainer?.Expire();
}
}
private void OnSkinChanged() private void OnSkinChanged()
{ {
if (lightContainer != null) if (hittingLayer != null)
{ {
column.TopLevelContainer.Remove(lightContainer, false); column.TopLevelContainer.Remove(hittingLayer, false);
lightContainer.Expire(); hittingLayer.Expire();
lightContainer = null; hittingLayer = null;
} }
loadAnimation();
hittingLayer = new EzHoldNoteHittingLayer hittingLayer = new EzHoldNoteHittingLayer
{ {
Alpha = 0, Alpha = 0,
IsHitting = { BindTarget = isHitting } IsHitting = { BindTarget = isHitting }
}; };
lightContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Alpha = 0,
Child = hittingLayer
};
hittingLayer.HitPosition.BindTo(hitPosition); hittingLayer.HitPosition.BindTo(hitPosition);
} }
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting) private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
{ {
hittingLayer.IsHitting.Value = isHitting.NewValue; if (hittingLayer != null) hittingLayer.IsHitting.Value = isHitting.NewValue;
if (lightContainer == null) if (hittingLayer == null)
return; return;
if (isHitting.NewValue) if (isHitting.NewValue)
{ {
lightContainer.ClearTransforms(); hittingLayer.ClearTransforms();
if (lightContainer.Parent == null) if (hittingLayer.Parent == null)
column.TopLevelContainer.Add(lightContainer); column.TopLevelContainer.Add(hittingLayer);
lightContainer.FadeIn(80); hittingLayer.FadeIn(80);
} }
else else
{ {
lightContainer.FadeOut(120) hittingLayer.FadeOut(120)
.OnComplete(d => column.TopLevelContainer.Remove(d, false)); .OnComplete(d => column.TopLevelContainer.Remove(d, false));
} }
} }
protected virtual Color4 NoteColor public void Recycle()
{ {
get ClearTransforms();
{ hittingLayer?.Recycle();
int keyMode = stageDefinition.Columns;
int columnIndex = column.Index;
return ezSkinConfig.GetColumnColor(keyMode, columnIndex);
}
} }
protected virtual string ColorPrefix protected override void OnDrawableChanged()
{ {
get string newComponentName = $"{ColorPrefix}note";
{ var body = Factory.CreateAnimation($"{ColorPrefix}longnote/middle");
if (enabledColor.Value) var tail = Factory.CreateAnimation($"{ColorPrefix}longnote/tail");
return "white";
if (ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) == "S1") if (body.FrameCount == 0)
return "green"; body = Factory.CreateAnimation(newComponentName);
int logicalIndex = 0; if (tail.FrameCount == 0)
tail = Factory.CreateAnimation(newComponentName);
for (int i = 0; i < column.Index; i++)
{
if (ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) != "S1")
logicalIndex++;
}
return logicalIndex % 2 == 0 ? "white" : "blue";
}
}
private void loadAnimation()
{
ClearInternal();
string backupComponentName = $"{ColorPrefix}note";
middleAnimation = factory.CreateAnimation($"{ColorPrefix}longnote/middle");
tailAnimation = factory.CreateAnimation($"{ColorPrefix}longnote/tail");
if (middleAnimation.FrameCount == 0)
{
middleAnimation.Dispose();
middleAnimation = factory.CreateAnimation(backupComponentName);
}
if (tailAnimation.FrameCount == 0)
{
tailAnimation.Dispose();
tailAnimation = factory.CreateAnimation(backupComponentName);
}
topContainer = new Container topContainer = new Container
{ {
@@ -218,67 +126,80 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Child = tailAnimation Child = tail
} }
}; };
middleContainer = new Container bodyContainer = new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Masking = true, Masking = true,
Child = middleScaleContainer = new Container Child = bodyScaleContainer = new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 1, Height = 1,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Child = middleInnerContainer = new Container Child = bodyInnerContainer = new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Child = middleAnimation Child = body
} }
} }
}; };
container = new Container
if (MainContainer != null)
{ {
RelativeSizeAxes = Axes.Both, MainContainer.Clear();
Children = new[] { middleContainer, topContainer } MainContainer.Children = [bodyContainer, topContainer];
};
OnSettingsChanged();
AddInternal(container);
}
private void updateSizes()
{
bool isSquare = factory.IsSquareNote("whitenote");
float noteSize = isSquare
? DrawWidth
: (float)(ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight).Value);
topContainer.Height = noteSize / 2;
if (topContainer.Child is Container topInner)
topInner.Height = noteSize;
float middleHeight = Math.Max(DrawHeight - noteSize / 2, noteSize / 2);
middleContainer.Y = noteSize / 2;
middleContainer.Height = middleHeight + 2;
middleScaleContainer.Scale = new Vector2(1, DrawHeight - noteSize / 2);
middleInnerContainer.Height = noteSize;
middleInnerContainer.Y = -noteSize / 2;
Invalidate();
}
private void OnSettingsChanged()
{
if (enabledColor.Value && container != null)
{
container.Colour = NoteColor;
} }
Schedule(updateSizes); Schedule(UpdateSize);
}
protected override void UpdateSize()
{
base.UpdateSize();
if (MainContainer?.Children.Count == 0)
return;
float v = NoteSize.Value.Y;
if (topContainer?.Child is Container topInner)
{
topContainer.Height = v / 2;
topInner.Height = v;
}
float middleHeight = Math.Max(DrawHeight - v / 2, v / 2);
if (bodyContainer != null)
{
bodyContainer.Y = v / 2;
bodyContainer.Height = middleHeight + 2;
}
if (bodyScaleContainer != null)
bodyScaleContainer.Scale = new Vector2(1, DrawHeight - v / 2);
if (bodyInnerContainer != null)
{
bodyInnerContainer.Height = v;
bodyInnerContainer.Y = -v / 2;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
hittingLayer?.Expire();
topContainer?.Expire();
bodyContainer?.Expire();
}
} }
} }
} }

View File

@@ -20,14 +20,12 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
public partial class EzHoldNoteTail : CompositeDrawable public partial class EzHoldNoteTail : CompositeDrawable
{ {
private EzHoldNoteHittingLayer hittingLayer = null!; private readonly EzHoldNoteHittingLayer hittingLayer = null!;
private TextureAnimation? animation; private TextureAnimation? animation;
private Container container = null!; private Container container = null!;
private EzSkinSettingsManager ezSkinConfig = null!; private EzSkinSettingsManager ezSkinConfig = null!;
private Bindable<bool> enabledColor = null!; private Bindable<bool> enabledColor = null!;
private float noteSize;
private bool isSquare;
[Resolved] [Resolved]
private Column column { get; set; } = null!; private Column column { get; set; } = null!;
@@ -48,9 +46,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Alpha = 0f; Alpha = 0f;
hittingLayer = new EzHoldNoteHittingLayer { RelativeSizeAxes = Axes.Both };
AddInternal(hittingLayer);
if (drawableObject != null) if (drawableObject != null)
{ {
// accentColour.BindTo(drawableObject.AccentColour); // accentColour.BindTo(drawableObject.AccentColour);
@@ -64,13 +59,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
private void updateSizes() private void updateSizes()
{ {
isSquare = factory.IsSquareNote("whitenote");
noteSize = isSquare
? DrawWidth
: (float)(ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight).Value);
container.Height = noteSize / 2;
if (animation != null) animation.Height = noteSize;
} }
protected override void LoadComplete() protected override void LoadComplete()
@@ -78,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
base.LoadComplete(); base.LoadComplete();
loadAnimation(); loadAnimation();
factory.OnNoteChanged += onSkinChanged; factory.OnNoteChanged += onSkinChanged;
ezSkinConfig.OnSettingsChanged += onSettingsChanged; factory.OnNoteSizeChanged += onNoteSizeChanged;
} }
private void onSkinChanged() private void onSkinChanged()
@@ -86,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
Schedule(loadAnimation); Schedule(loadAnimation);
} }
private void onSettingsChanged() private void onNoteSizeChanged()
{ {
if (enabledColor.Value) if (enabledColor.Value)
container.Colour = NoteColor; container.Colour = NoteColor;
@@ -145,7 +133,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
} }
}; };
onSettingsChanged(); onNoteSizeChanged();
AddInternal(container); AddInternal(container);
} }

View File

@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
base.LoadComplete(); base.LoadComplete();
OnSkinChanged(); OnSkinChanged();
factory.OnStageChanged += OnSkinChanged; factory.OnStageChanged += OnSkinChanged;
ezSkinConfig.OnSettingsChanged += OnConfigChanged; factory.OnNoteSizeChanged += OnConfigChanged;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
@@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
if (isDisposing) if (isDisposing)
{ {
factory.OnStageChanged -= OnSkinChanged; factory.OnStageChanged -= OnSkinChanged;
ezSkinConfig.OnSettingsChanged -= OnConfigChanged; factory.OnNoteSizeChanged -= OnConfigChanged;
} }
} }
@@ -67,14 +67,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
get get
{ {
if (ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) == "S1") if (ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) == "S")
return "02"; return "02";
int logicalIndex = 0; int logicalIndex = 0;
for (int i = 0; i < column.Index; i++) for (int i = 0; i < column.Index; i++)
{ {
if (ezSkinConfig.GetColumnType(stageDefinition.Columns, i) == "S1") if (ezSkinConfig.GetColumnType(stageDefinition.Columns, i) == "S")
logicalIndex++; logicalIndex++;
} }
@@ -107,21 +107,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
AddInternal(container); AddInternal(container);
} }
private void updateSizes()
{
bool isSquare = factory.IsSquareNote("whitenote");
Height = isSquare
? DrawWidth
: (float)(ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight).Value);
}
private void OnConfigChanged() private void OnConfigChanged()
{ {
Schedule(() =>
{
updateSizes();
Invalidate();
});
} }
private void OnSkinChanged() => loadAnimation(); private void OnSkinChanged() => loadAnimation();

View File

@@ -2,181 +2,48 @@
// 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.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Screens;
using osu.Game.Screens.LAsEzExtensions;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
public partial class EzNote : CompositeDrawable public partial class EzNote : EzNoteBase
{ {
private Bindable<bool> enabledColor = null!; protected override bool ShowSeparators => true;
private Bindable<double> nonSquareNoteHeight = null!;
private TextureAnimation animation = null!;
private Drawable container = null!;
private EzNoteSideLine? noteSeparatorsL;
private EzNoteSideLine? noteSeparatorsR;
protected virtual bool ShowSeparators => true;
protected virtual bool UseColorization => true;
[Resolved]
private Column column { get; set; } = null!;
[Resolved]
private StageDefinition stageDefinition { get; set; } = null!;
[Resolved]
private EzLocalTextureFactory factory { get; set; } = null!;
[Resolved]
private EzSkinSettingsManager ezSkinConfig { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
FillMode = FillMode.Fill; FillMode = FillMode.Fill;
enabledColor = ezSkinConfig.GetBindable<bool>(EzSkinSetting.ColorSettingsEnabled);
nonSquareNoteHeight = ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight);
} }
protected override void LoadComplete() protected override void OnDrawableChanged()
{ {
base.LoadComplete(); string newComponentName = $"{ColorPrefix}note";
OnSkinChanged();
enabledColor.BindValueChanged(_ => OnConfigChanged(), true);
nonSquareNoteHeight.BindValueChanged(_ => updateSizes(), true);
ezSkinConfig.OnSettingsChanged += OnConfigChanged;
factory.OnNoteChanged += OnSkinChanged;
}
protected override void Dispose(bool isDisposing) var animation = Factory.CreateAnimation(newComponentName);
{
base.Dispose(isDisposing);
if (isDisposing) if (animation is TextureAnimation textureAnimation && textureAnimation.FrameCount == 0)
{ {
ezSkinConfig.OnSettingsChanged -= OnConfigChanged; animation.Dispose();
factory.OnNoteChanged -= OnSkinChanged; UpdateColor();
} return;
}
protected virtual Color4 NoteColor
{
get
{
int keyMode = stageDefinition.Columns;
int columnIndex = column.Index;
return ezSkinConfig.GetColumnColor(keyMode, columnIndex);
}
}
protected virtual string ColorPrefix
{
get
{
if (enabledColor.Value)
return "white";
if (ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) == "S1")
return "green";
int logicalIndex = 0;
for (int i = 0; i < column.Index; i++)
{
if (ezSkinConfig.GetColumnType(stageDefinition.Columns, i) != "S1")
logicalIndex++;
}
return logicalIndex % 2 == 0 ? "white" : "blue";
}
}
protected virtual string ComponentName => $"{ColorPrefix}note";
private void loadAnimation()
{
ClearInternal();
animation = factory.CreateAnimation(ComponentName);
container = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = animation
};
if (ShowSeparators)
{
noteSeparatorsL = new EzNoteSideLine
{
RelativeSizeAxes = Axes.X,
FillMode = FillMode.Fill,
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
};
noteSeparatorsR = new EzNoteSideLine
{
RelativeSizeAxes = Axes.X,
FillMode = FillMode.Fill,
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre,
// Rotation = 180,
};
AddInternal(new Container
{
RelativeSizeAxes = Axes.X,
FillMode = FillMode.Stretch,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
noteSeparatorsL,
noteSeparatorsR
}
});
} }
OnConfigChanged(); if (MainContainer != null)
AddInternal(container);
}
private void updateSizes()
{
bool isSquare = factory.IsSquareNote("whitenote");
Height = isSquare
? DrawWidth
: (float)(ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight).Value);
}
private void OnConfigChanged()
{
var noteColor = Color4.White;
if (enabledColor.Value && UseColorization)
noteColor = NoteColor;
container.Colour = noteColor;
noteSeparatorsL?.UpdateGlowEffect(noteColor);
noteSeparatorsR?.UpdateGlowEffect(noteColor);
Schedule(() =>
{ {
updateSizes(); MainContainer.Clear();
Invalidate(); MainContainer.Child = animation;
}); }
Schedule(UpdateSize);
} }
private void OnSkinChanged() => loadAnimation(); protected override void UpdateSize()
{
base.UpdateSize();
Height = NoteSize.Value.Y;
}
} }
} }

View File

@@ -0,0 +1,171 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Screens;
using osu.Game.Screens.LAsEzExtensions;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
public abstract partial class EzNoteBase : CompositeDrawable
{
protected virtual bool BoolUpdateColor => true;
protected virtual bool UseColorization => true;
protected virtual bool ShowSeparators => false;
protected int KeyMode;
protected int ColumnIndex;
protected Container? SeparatorsContainer { get; private set; }
protected Container? MainContainer { get; private set; }
[Resolved]
protected Column Column { get; private set; } = null!;
[Resolved]
protected StageDefinition StageDefinition { get; private set; } = null!;
[Resolved]
protected EzSkinSettingsManager EZSkinConfig { get; private set; } = null!;
[Resolved]
protected EzLocalTextureFactory Factory { get; private set; } = null!;
private IBindable<Colour4> columnColorBindable = null!;
protected Bindable<bool> EnabledColor = null!;
protected Bindable<Vector2> NoteSize = null!;
[BackgroundDependencyLoader]
private void load()
{
KeyMode = StageDefinition.Columns;
ColumnIndex = Column.Index;
EnabledColor = EZSkinConfig.GetBindable<bool>(EzSkinSetting.ColorSettingsEnabled);
columnColorBindable = EZSkinConfig.GetColumnColorBindable(KeyMode, ColumnIndex);
NoteSize = Factory.GetNoteSize(KeyMode, ColumnIndex);
createSeparators();
MainContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
AddInternal(MainContainer);
}
private void createSeparators()
{
var noteSeparatorsL = new EzNoteSideLine
{
RelativeSizeAxes = Axes.X,
FillMode = FillMode.Fill,
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
};
var noteSeparatorsR = new EzNoteSideLine
{
RelativeSizeAxes = Axes.X,
FillMode = FillMode.Fill,
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre,
};
SeparatorsContainer = new Container
{
RelativeSizeAxes = Axes.X,
FillMode = FillMode.Stretch,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = ShowSeparators ? 1f : 0f,
Children = [noteSeparatorsL, noteSeparatorsR]
};
AddInternal(SeparatorsContainer);
}
protected override void LoadComplete()
{
base.LoadComplete();
EnabledColor.BindValueChanged(_ => UpdateColor(), true);
columnColorBindable.BindValueChanged(_ => UpdateColor(), true);
NoteSize.BindValueChanged(_ => UpdateSize(), true);
Factory.OnNoteChanged += OnDrawableChanged;
// Factory.OnNoteSizeChanged += UpdateSize;
Scheduler.AddOnce(OnDrawableChanged);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Factory.OnNoteChanged -= OnDrawableChanged;
// Factory.OnNoteSizeChanged -= UpdateSize;
}
protected Colour4 NoteColor
{
get
{
if (EnabledColor.Value && UseColorization)
return EZSkinConfig.GetColumnColor(KeyMode, ColumnIndex);
return Colour4.White;
}
}
protected virtual string ColorPrefix
{
get
{
if (EnabledColor.Value) return "white";
string keyType = EZSkinConfig.GetColumnType(KeyMode, ColumnIndex);
return keyType switch
{
"A" => "white",
"B" => "blue",
"S" => "green",
"E" => "white",
"P" => "green",
_ => "white"
};
}
}
protected void UpdateColor()
{
if (BoolUpdateColor)
{
if (MainContainer != null)
MainContainer.Colour = NoteColor;
if (SeparatorsContainer?.Children != null)
{
foreach (var child in SeparatorsContainer.Children)
{
if (child is EzNoteSideLine sideLine)
sideLine.UpdateGlowEffect(NoteColor);
}
}
}
}
protected virtual void UpdateSize()
{
NoteSize = Factory.GetNoteSize(KeyMode, ColumnIndex);
UpdateColor();
}
protected virtual void OnDrawableChanged() { }
}
}

View File

@@ -3,7 +3,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@@ -11,7 +10,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Screens; using osu.Game.Screens;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
@@ -74,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
separator.Height = (float)noteTrackLineHeight.Value; separator.Height = (float)noteTrackLineHeight.Value;
} }
public void UpdateGlowEffect(Color4 color) public void UpdateGlowEffect(Colour4 color)
{ {
separator.Colour = new ColourInfo separator.Colour = new ColourInfo
{ {

View File

@@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
hitPositon = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition); hitPositon = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
columnWidth = ezSkinConfig.GetBindable<double>(EzSkinSetting.ColumnWidth); columnWidth = ezSkinConfig.GetBindable<double>(EzSkinSetting.ColumnWidth);
hitPositon.BindValueChanged(_ => OnConfigChanged()); hitPositon.BindValueChanged(_ => updateSizes());
columnWidth.BindValueChanged(_ => OnConfigChanged()); columnWidth.BindValueChanged(_ => updateSizes());
OnSkinChanged(); OnSkinChanged();
} }
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
// updateSizes(); // updateSizes();
// } // }
private void loadAnimation() private void OnSkinChanged()
{ {
ClearInternal(); ClearInternal();
@@ -77,8 +77,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
Child = stageBottom Child = stageBottom
}; };
// sprite.Depth = float.MinValue; // sprite.Depth = float.MinValue;
OnConfigChanged();
AddInternal(sprite); AddInternal(sprite);
Schedule(updateSizes);
} }
private void updateSizes() private void updateSizes()
@@ -93,16 +93,5 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
sprite.Y = LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION - (float)hitPositon.Value - DrawHeight * 0.865f; sprite.Y = LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION - (float)hitPositon.Value - DrawHeight * 0.865f;
// Position = new Vector2(0, 415 + 110 - (float)hitPositon.Value); // Position = new Vector2(0, 415 + 110 - (float)hitPositon.Value);
} }
private void OnConfigChanged()
{
Schedule(() =>
{
updateSizes();
Invalidate();
});
}
private void OnSkinChanged() => loadAnimation();
} }
} }

View File

@@ -202,6 +202,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
case ManiaSkinComponents.HitExplosion: case ManiaSkinComponents.HitExplosion:
return new EzHitExplosion(); return new EzHitExplosion();
// return HitExplosionPool.Rent();
case ManiaSkinComponents.StageBackground: case ManiaSkinComponents.StageBackground:
return new Ez2StageBackground(); return new Ez2StageBackground();
@@ -226,7 +227,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{ {
int columnIndex = maniaLookup.ColumnIndex ?? 0; int columnIndex = maniaLookup.ColumnIndex ?? 0;
var stage = beatmap.GetStageForColumnIndex(columnIndex); var stage = beatmap.GetStageForColumnIndex(columnIndex);
bool isSpecialColumn = ezSkinConfig.GetColumnType(stage.Columns, columnIndex) == "S1"; bool isSpecialColumn = ezSkinConfig.GetColumnType(stage.Columns, columnIndex) == "S";
float width = (float)columnWidthBindable.Value * (isSpecialColumn ? (float)specialFactorBindable.Value : 1f); float width = (float)columnWidthBindable.Value * (isSpecialColumn ? (float)specialFactorBindable.Value : 1f);
// float hitPositionValue = (float)hitPosition.Value; // + (float)virtualHitPosition.Value - 110f; // float hitPositionValue = (float)hitPosition.Value; // + (float)virtualHitPosition.Value - 110f;

View File

@@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private Bindable<double> hitPositonBindable = new Bindable<double>(); private Bindable<double> hitPositonBindable = new Bindable<double>();
private Bindable<bool> globalHitPosition = new Bindable<bool>();
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
private readonly Container content; private readonly Container content;
@@ -30,17 +32,23 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin, EzSkinSettingsManager ezSkinConfig, IScrollingInfo scrollingInfo) private void load(ISkinSource skin, EzSkinSettingsManager ezSkinConfig, IScrollingInfo scrollingInfo)
{ {
hitPositonBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
hitPositonBindable.BindValueChanged(_ => UpdateHitPosition(), true);
hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? (float)hitPositonBindable.Value;
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true); direction.BindValueChanged(onDirectionChanged, true);
ezSkinConfig.GetBindable<bool>(EzSkinSetting.GlobalHitPosition).BindValueChanged(_ => updateHitPosition(skin, ezSkinConfig));
ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition).BindValueChanged(_ => updateHitPosition(skin, ezSkinConfig));
updateHitPosition(skin, ezSkinConfig);
} }
protected virtual void UpdateHitPosition() private void updateHitPosition(ISkinSource skin, EzSkinSettingsManager ezSkinConfig)
{ {
hitPosition = (float)hitPositonBindable.Value; bool globalHitPositionValue = ezSkinConfig.GetBindable<bool>(EzSkinSetting.GlobalHitPosition).Value;
double hitPositionValue = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition).Value;
hitPosition = globalHitPositionValue
? (float)hitPositionValue
: skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? (float)hitPositionValue;
} }
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction) private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)

View File

@@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Mania.UI
} }
bool isSpecialColumn = bool isSpecialColumn =
ezSkinConfig.GetColumnType(stageDefinition.Columns, i) == "S1"; ezSkinConfig.GetColumnType(stageDefinition.Columns, i) == "S";
float ezWidth = (float)columnWidthBindable.Value * (isSpecialColumn ? (float)specialFactorBindable.Value : 1); float ezWidth = (float)columnWidthBindable.Value * (isSpecialColumn ? (float)specialFactorBindable.Value : 1);
switch (ezColumnWidthStyle.Value) switch (ezColumnWidthStyle.Value)

View File

@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private EzSkinSettingsManager ezSkinConfig { get; set; } = null!; private EzSkinSettingsManager ezSkinConfig { get; set; } = null!;
private Bindable<double> hitPositonBindable = new Bindable<double>(); private Bindable<double> hitPositonBindable = new Bindable<double>();
private readonly Bindable<bool> globalHitPosition = new Bindable<bool>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo) private void load(IScrollingInfo scrollingInfo)
@@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
Direction.BindTo(scrollingInfo.Direction); Direction.BindTo(scrollingInfo.Direction);
Direction.BindValueChanged(_ => UpdateHitPosition(), true); Direction.BindValueChanged(_ => UpdateHitPosition(), true);
ezSkinConfig.BindWith(EzSkinSetting.GlobalHitPosition, globalHitPosition);
globalHitPosition.BindValueChanged(_ => UpdateHitPosition(), true);
hitPositonBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition); hitPositonBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
hitPositonBindable.BindValueChanged(_ => UpdateHitPosition(), true); hitPositonBindable.BindValueChanged(_ => UpdateHitPosition(), true);
skin.SourceChanged += onSkinChanged; skin.SourceChanged += onSkinChanged;
@@ -40,9 +43,16 @@ namespace osu.Game.Rulesets.Mania.UI.Components
protected virtual void UpdateHitPosition() protected virtual void UpdateHitPosition()
{ {
float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>( float hitPosition;
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? (float)hitPositonBindable.Value; if (globalHitPosition.Value)
hitPosition = (float)hitPositonBindable.Value;
else
{
hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? (float)hitPositonBindable.Value;
}
Padding = Direction.Value == ScrollingDirection.Up Padding = Direction.Value == ScrollingDirection.Up
? new MarginPadding { Top = hitPosition } ? new MarginPadding { Top = hitPosition }

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset(); var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate }; var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate)); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
} }
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset(); var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty }; var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty)); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
} }
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset(); var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty(); var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new OsuModHalfTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01)); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(1.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01)); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
@@ -56,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset(); var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty(); var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new OsuModDoubleTime()]);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01)); Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(7.67).Within(0.01));
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01)); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(7.77).Within(0.01));

View File

@@ -21,6 +21,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@@ -152,6 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests
} }
[Test] [Test]
[FlakyTest]
public void TestSpinPerMinuteOnRewind() public void TestSpinPerMinuteOnRewind()
{ {
double estimatedSpm = 0; double estimatedSpm = 0;

View File

@@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
overallDifficulty = (80 - greatHitWindow) / 6; overallDifficulty = (79.5 - greatHitWindow) / 6;
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5; approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
if (osuAttributes.SliderCount > 0) if (osuAttributes.SliderCount > 0)

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
@@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModEasy : ModEasyWithExtraLives public class OsuModEasy : ModEasyWithExtraLives
{ {
public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!"; public override LocalisableString Description => @"Larger circles, more forgiving HP drain, less accuracy required, and extra lives!";
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
}
} }
} }

View File

@@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
base.ApplyToDifficulty(difficulty); base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f); difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
} }

View File

@@ -12,9 +12,9 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@@ -83,7 +83,12 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
} }
public override Judgement CreateJudgement() => new OsuJudgement(); public override Judgement CreateJudgement() => new StrictTrackingTailJudgement();
}
public class StrictTrackingTailJudgement : SliderTailCircle.TailJudgement
{
public override HitResult MinResult => HitResult.LargeTickMiss;
} }
private partial class StrictTrackingDrawableSliderTail : DrawableSliderTail private partial class StrictTrackingDrawableSliderTail : DrawableSliderTail

View File

@@ -40,6 +40,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
@@ -365,9 +366,10 @@ namespace osu.Game.Rulesets.Osu
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/> /// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
/// <seealso cref="OsuHitWindows"/> /// <seealso cref="OsuHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{ {
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN); double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
preempt /= rate; preempt /= rate;

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Mods;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests
{ {
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset(); var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty }; var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty)); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
} }
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset(); var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty(); var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 0.75); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModHalfTime()]);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01)); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(1.11).Within(0.01));
} }
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset(); var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty(); var difficulty = new BeatmapDifficulty();
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1.5); var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, [new TaikoModDoubleTime()]);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01)); Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(8.89).Within(0.01));
} }

View File

@@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override void ApplyToDifficulty(BeatmapDifficulty difficulty) public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{ {
base.ApplyToDifficulty(difficulty); base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty *= ADJUST_RATIO;
difficulty.SliderMultiplier *= slider_multiplier; difficulty.SliderMultiplier *= slider_multiplier;
} }
} }

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override void ApplyToDifficulty(BeatmapDifficulty difficulty) public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{ {
base.ApplyToDifficulty(difficulty); base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.SliderMultiplier *= slider_multiplier; difficulty.SliderMultiplier *= slider_multiplier;
} }
} }

View File

@@ -38,6 +38,7 @@ using osu.Game.Rulesets.Taiko.Configuration;
using osu.Game.Rulesets.Taiko.Edit.Setup; using osu.Game.Rulesets.Taiko.Edit.Setup;
using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Taiko namespace osu.Game.Rulesets.Taiko
{ {
@@ -270,9 +271,10 @@ namespace osu.Game.Rulesets.Taiko
} }
/// <seealso cref="TaikoHitWindows"/> /// <seealso cref="TaikoHitWindows"/>
public override BeatmapDifficulty GetRateAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, double rate) public override BeatmapDifficulty GetAdjustedDisplayDifficulty(IBeatmapDifficultyInfo difficulty, IReadOnlyCollection<Mod> mods)
{ {
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty); BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(difficulty);
double rate = ModUtils.CalculateRateWithMods(mods);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE); double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE);
greatHitWindow /= rate; greatHitWindow /= rate;

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using ManagedBass;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
@@ -10,7 +11,9 @@ using osu.Game.Beatmaps;
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;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK.Audio;
namespace osu.Game.Tests.Editing.Checks namespace osu.Game.Tests.Editing.Checks
{ {
@@ -28,9 +31,13 @@ namespace osu.Game.Tests.Editing.Checks
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
Metadata = new BeatmapMetadata { AudioFile = "abc123.jpg" } Metadata = new BeatmapMetadata()
} }
}; };
// 0 = No output device. This still allows decoding.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
throw new AudioException("Could not initialize Bass.");
} }
[Test] [Test]
@@ -54,6 +61,14 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(check.Run(context), Is.Empty); Assert.That(check.Run(context), Is.Empty);
} }
[Test]
public void TestAcceptableOgg()
{
var context = getContext(208, useOgg: true);
Assert.That(check.Run(context), Is.Empty);
}
[Test] [Test]
public void TestNullBitrate() public void TestNullBitrate()
{ {
@@ -87,6 +102,17 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate); Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
} }
[Test]
public void TestTooHighBitrateOgg()
{
var context = getContext(250, useOgg: true);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
}
[Test] [Test]
public void TestTooLowBitrate() public void TestTooLowBitrate()
{ {
@@ -98,24 +124,41 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate); Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate);
} }
private BeatmapVerifierContext getContext(int? audioBitrate) private BeatmapVerifierContext getContext(int? audioBitrate, bool useOgg = false)
{ {
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate).Object); // Update the audio filename and beatmapset files based on the format being tested
string audioFileName = useOgg ? "abc123.ogg" : "abc123.mp3";
string fileExtension = useOgg ? "ogg" : "mp3";
beatmap.Metadata.AudioFile = audioFileName;
beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo
{
Files = { CheckTestHelpers.CreateMockFile(fileExtension) }
};
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate, useOgg).Object);
} }
/// <summary> /// <summary>
/// Returns the mock of the working beatmap with the given audio properties. /// Returns the mock of the working beatmap with the given audio properties.
/// </summary> /// </summary>
/// <param name="audioBitrate">The bitrate of the audio file the beatmap uses.</param> /// <param name="audioBitrate">The bitrate of the audio file the beatmap uses.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate) /// <param name="useOgg">Whether to use an OGG sample instead of MP3.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(int? audioBitrate, bool useOgg = false)
{ {
var mockTrack = new Mock<OsuTestScene.ClockBackedTestWorkingBeatmap.TrackVirtualManual>(new FramedClock(), "virtual"); var mockTrack = new Mock<OsuTestScene.ClockBackedTestWorkingBeatmap.TrackVirtualManual>(new FramedClock(), "virtual");
mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate); mockTrack.SetupGet(t => t.Bitrate).Returns(audioBitrate);
// Use real audio samples for format detection
string samplePath = useOgg ? "Samples/test-sample.ogg" : "Samples/test-sample-cut.mp3";
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>(); var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap); mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack.Object); mockWorkingBeatmap.SetupGet(w => w.Track).Returns(mockTrack.Object);
// Return a fresh stream each time GetStream is called to avoid disposed stream issues
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(() => TestResources.OpenResource(samplePath));
return mockWorkingBeatmap; return mockWorkingBeatmap;
} }
} }

View File

@@ -136,14 +136,39 @@ namespace osu.Game.Tests.NonVisual
AddUntilStep("no check pending", () => !manager.IsPending); AddUntilStep("no check pending", () => !manager.IsPending);
} }
[Test]
public void TestFixedReleaseStreamWrittenToConfig()
{
AddStep("add manager", () =>
{
config = new OsuConfigManager(LocalStorage);
config.SetValue(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
Child = new DependencyProvidingContainer
{
CachedDependencies = [(typeof(OsuConfigManager), config)],
Child = manager = new TestUpdateManager(ReleaseStream.Tachyon)
};
});
AddAssert("release stream set to tachyon", () => config.Get<ReleaseStream>(OsuSetting.ReleaseStream), () => Is.EqualTo(ReleaseStream.Tachyon));
}
private partial class TestUpdateManager : UpdateManager private partial class TestUpdateManager : UpdateManager
{ {
public override ReleaseStream? FixedReleaseStream { get; }
public bool IsPending { get; private set; } public bool IsPending { get; private set; }
public int Invocations { get; private set; } public int Invocations { get; private set; }
public int Completions { get; private set; } public int Completions { get; private set; }
private TaskCompletionSource<bool>? pendingCheck; private TaskCompletionSource<bool>? pendingCheck;
public TestUpdateManager(ReleaseStream? fixedReleaseStream = null)
{
FixedReleaseStream = fixedReleaseStream;
}
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken) protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
{ {
Invocations++; Invocations++;

View File

@@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Visual.Menus
{
public partial class TestSceneToolbarRulesetSelector : OsuTestScene
{
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets, OsuGameBase game)
{
TestRuleset.Resources = new TestResourceStore(game.Resources);
Dependencies.CacheAs<RulesetStore>(new TestRulesetStore(rulesets));
Child = new Container
{
RelativeSizeAxes = Axes.X,
Height = Toolbar.HEIGHT,
Child = new ToolbarRulesetSelector(),
};
}
private class TestRulesetStore : RulesetStore
{
public TestRulesetStore(RulesetStore store)
{
AvailableRulesets = store.AvailableRulesets.Append(new TestRuleset().RulesetInfo);
}
public override IEnumerable<RulesetInfo> AvailableRulesets { get; }
}
private class TestRuleset : Ruleset
{
public static IResourceStore<byte[]> Resources { get; set; } = null!;
public override IEnumerable<Mod> GetModsFor(ModType type) => Enumerable.Empty<Mod>();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => null!;
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
public override IResourceStore<byte[]> CreateResourceStore() => Resources;
public override string Description => "Test Ruleset";
public override string ShortName => "test";
}
private class TestResourceStore : ResourceStore<byte[]>
{
public TestResourceStore(IResourceStore<byte[]> store)
: base(store)
{
}
protected override IEnumerable<string> GetFilenames(string name) => base.GetFilenames(name)
.Select(s => s.Replace("UI/ruleset-select-test", "Gameplay/failsound"));
}
}
}

View File

@@ -53,6 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
private BeatmapManager beatmaps = null!; private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!; private BeatmapSetInfo importedSet = null!;
private BeatmapSetInfo importedSet2 = null!;
private TestMultiplayerComponents multiplayerComponents = null!; private TestMultiplayerComponents multiplayerComponents = null!;
@@ -81,12 +82,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () => AddStep("import beatmap", () =>
{ {
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
importedSet2 = beatmaps.Import(CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet!)!.Value.Detach();
Realm.Write(r => Realm.Write(r =>
{ {
foreach (var beatmapInfo in r.All<BeatmapInfo>()) foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash; beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
}); });
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
}); });
AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents())); AddStep("load multiplayer", () => LoadScreen(multiplayerComponents = new TestMultiplayerComponents()));
@@ -1095,6 +1099,112 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("global beatmap matches second playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID)); AddAssert("global beatmap matches second playlist item", () => Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(multiplayerClient.ClientRoom!.Playlist[1].BeatmapID));
} }
/// <summary>
/// Tests that the local user is not able to change their play style if they haven't downloaded the beatmap (beatmap carousel will be empty).
/// </summary>
[Test]
public void TestCanNotEditDifficultyIfNotDownloaded()
{
IBeatmap roomBeatmap = null!;
createRoom(() =>
{
roomBeatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
return new Room
{
Name = "Test Room",
QueueMode = QueueMode.AllPlayers,
Playlist =
[
new PlaylistItem(CreateAPIBeatmap(roomBeatmap.BeatmapInfo))
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Freestyle = true
}
]
};
});
AddAssert("editing disallowed", () => !this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().UserStyleEditingEnabled);
AddStep("import beatmap", () => beatmaps.Import(roomBeatmap.BeatmapInfo.BeatmapSet!));
AddAssert("editing allowed", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().UserStyleEditingEnabled);
}
/// <summary>
/// Test that the user selection screen is not exited when the beatmap is changed to the same set.
/// </summary>
[Test]
public void TestUserStyleSelectionDoesNotExitWhenBeatmapSetNotChanged()
{
createRoom(() => new Room
{
Name = "Test Room",
QueueMode = QueueMode.AllPlayers,
Playlist =
[
new PlaylistItem(importedSet.Beatmaps.First())
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Freestyle = true
}
]
});
AddStep("open user style selection", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().ShowUserStyleSelect());
AddUntilStep("style selection screen opened", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() == true);
AddStep("change beatmap", () =>
{
var newItem = multiplayerClient.ServerRoom!.Playlist[0].Clone();
var newBeatmap = importedSet.Beatmaps.Last();
newItem.BeatmapID = newBeatmap.OnlineID;
newItem.BeatmapChecksum = newBeatmap.MD5Hash;
multiplayerClient.EditPlaylistItem(newItem);
});
AddWaitStep("wait for potential beatmap change", 2);
AddAssert("style selection screen still open", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() == true);
}
/// <summary>
/// Tests that the user selection screen is exited when the beatmap is changed to another set.
/// </summary>
[Test]
public void TestUserStyleSelectionExitedWhenBeatmapSetChanged()
{
createRoom(() => new Room
{
Name = "Test Room",
QueueMode = QueueMode.AllPlayers,
Playlist =
[
new PlaylistItem(importedSet.Beatmaps.First())
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Freestyle = true
}
]
});
AddStep("open user style selection", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().ShowUserStyleSelect());
AddUntilStep("style selection screen opened", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() == true);
AddStep("change beatmap set", () =>
{
var newItem = multiplayerClient.ServerRoom!.Playlist[0].Clone();
var newBeatmap = importedSet2.Beatmaps.Last();
newItem.BeatmapID = newBeatmap.OnlineID;
newItem.BeatmapChecksum = newBeatmap.MD5Hash;
multiplayerClient.EditPlaylistItem(newItem);
});
AddUntilStep("selected beatmap changed", () => Beatmap.Value.BeatmapInfo.Equals(importedSet2.Beatmaps.First()));
AddUntilStep("style selection screen closed", () => this.ChildrenOfType<MultiplayerMatchFreestyleSelect>().SingleOrDefault()?.IsCurrentScreen() != true);
}
private void enterGameplay() private void enterGameplay()
{ {
pressReadyButton(); pressReadyButton();

View File

@@ -13,6 +13,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@@ -53,10 +54,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
Dependencies.CacheAs<BeatmapStore>(new RealmDetachedBeatmapStore());
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First(); importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
} }
public override void SetUpSteps() public override void SetUpSteps()

View File

@@ -471,7 +471,7 @@ namespace osu.Game.Tests.Visual.Navigation
{ {
AddUntilStep($"config value is {configValue}", () => getConfigManager().Get<double>(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue)); AddUntilStep($"config value is {configValue}", () => getConfigManager().Get<double>(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue));
AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType<DrawableManiaRuleset>().Single().TargetTimeRange, AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType<DrawableManiaRuleset>().Single().TargetTimeRange,
() => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue))); () => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue, 200, 5)));
} }
ManiaRulesetConfigManager getConfigManager() => ((ManiaRulesetConfigManager)Game.Dependencies.Get<IRulesetConfigCache>().GetConfigFor(new ManiaRuleset())!); ManiaRulesetConfigManager getConfigManager() => ((ManiaRulesetConfigManager)Game.Dependencies.Get<IRulesetConfigCache>().GetConfigFor(new ManiaRuleset())!);

View File

@@ -10,6 +10,8 @@ using osu.Game.Rulesets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@@ -193,7 +195,8 @@ namespace osu.Game.Tests.Visual.Online
overlay.ShowBeatmapSet(set); overlay.ShowBeatmapSet(set);
}); });
AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.OnlineID == overlay.Header.RulesetSelector.Current.Value.OnlineID)); AddAssert("shown beatmaps of current ruleset",
() => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.OnlineID == overlay.Header.RulesetSelector.Current.Value.OnlineID));
AddAssert("left-most beatmap selected", () => overlay.Header.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected); AddAssert("left-most beatmap selected", () => overlay.Header.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected);
} }
@@ -373,6 +376,39 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
[Test]
public void TestBeatmapsetWithDeletedUser()
{
AddStep("show map with deleted user", () =>
{
JObject jsonBlob = JObject.FromObject(getBeatmapSet(), new JsonSerializer
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
jsonBlob["user"] = JToken.Parse(
"""
{
"avatar_url": null,
"country_code": null,
"default_group": "default",
"id": null,
"is_active": false,
"is_bot": false,
"is_deleted": true,
"is_online": false,
"is_supporter": false,
"last_visit": null,
"pm_friends_only": false,
"profile_colour": null,
"username": "[deleted user]"
}
""");
overlay.ShowBeatmapSet(JsonConvert.DeserializeObject<APIBeatmapSet>(JsonConvert.SerializeObject(jsonBlob)));
});
}
private APIBeatmapSet createManyDifficultiesBeatmapSet() private APIBeatmapSet createManyDifficultiesBeatmapSet()
{ {
var set = getBeatmapSet(); var set = getBeatmapSet();

View File

@@ -274,6 +274,29 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("all panels have non-negative position", () => this.ChildrenOfType<ScorePanel>().All(p => p.ScorePosition.Value > 0)); AddUntilStep("all panels have non-negative position", () => this.ChildrenOfType<ScorePanel>().All(p => p.ScorePosition.Value > 0));
} }
[Test]
public void TestPresentInvalidOnlineScore()
{
AddStep("set up invalid user score", () =>
{
userScore.OnlineID = -1;
userScore.TotalScore = 0;
});
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResultsWithScore(() => userScore);
AddUntilStep("wait for user score to be displayed", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Any());
AddWaitStep("wait for any more potential scores", 5);
AddAssert("only 1 score visible", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Count(), () => Is.EqualTo(1));
AddUntilStep("left loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden);
AddUntilStep("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
}
private void createResultsWithScore(Func<ScoreInfo> getScore) private void createResultsWithScore(Func<ScoreInfo> getScore)
{ {
AddStep("load results", () => AddStep("load results", () =>
@@ -359,7 +382,7 @@ namespace osu.Game.Tests.Visual.Playlists
switch (request) switch (request)
{ {
case ShowPlaylistScoreRequest s: case ShowPlaylistScoreRequest s:
if (userScore == null) if (userScore == null || userScore.OnlineID == -1)
triggerFail(s); triggerFail(s);
else else
triggerSuccess(s, () => createUserResponse(userScore)); triggerSuccess(s, () => createUserResponse(userScore));
@@ -367,7 +390,7 @@ namespace osu.Game.Tests.Visual.Playlists
break; break;
case ShowPlaylistUserScoreRequest u: case ShowPlaylistUserScoreRequest u:
if (userScore == null) if (userScore == null || userScore.OnlineID == -1)
triggerFail(u); triggerFail(u);
else else
triggerSuccess(u, () => createUserResponse(userScore)); triggerSuccess(u, () => createUserResponse(userScore));

View File

@@ -333,22 +333,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2
#endregion #endregion
#region Source grouping
[Test]
public async Task TestGroupingBySource()
{
int total = 0;
var beatmapSets = new List<BeatmapSetInfo>();
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Cool Game", beatmapSets, out var beatmapCoolGame);
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Cool game", beatmapSets, out var beatmapCoolGameB);
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = "Nice Movie", beatmapSets, out var beatmapNiceMovie);
addBeatmapSet(s => s.Beatmaps[0].Metadata.Source = string.Empty, beatmapSets, out var beatmapUnsourced);
var results = await runGrouping(GroupMode.Source, beatmapSets);
assertGroup(results, 0, "Cool Game", new[] { beatmapCoolGame, beatmapCoolGameB }, ref total);
assertGroup(results, 1, "Nice Movie", new[] { beatmapNiceMovie }, ref total);
assertGroup(results, 2, "Unsourced", new[] { beatmapUnsourced }, ref total);
assertTotal(results, total);
}
#endregion
private static async Task<List<CarouselItem>> runGrouping(GroupMode group, List<BeatmapSetInfo> beatmapSets) private static async Task<List<CarouselItem>> runGrouping(GroupMode group, List<BeatmapSetInfo> beatmapSets)
{ {
var groupingFilter = new BeatmapCarouselFilterGrouping(() => new FilterCriteria { Group = group }); var groupingFilter = new BeatmapCarouselFilterGrouping(() => new FilterCriteria { Group = group });
var carouselItems = await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None); return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None);
// sanity check to ensure no detection of two group items with equal order value.
var groups = carouselItems.Select(i => i.Model).OfType<GroupDefinition>();
foreach (var header in groups)
{
var sameOrder = groups.FirstOrDefault(g => g != header && g.Order == header.Order);
if (sameOrder != null)
Assert.Fail($"Detected two groups with equal order number: \"{header.Title}\" vs. \"{sameOrder.Title}\"");
}
return carouselItems;
} }
private static void assertGroup(List<CarouselItem> items, int index, string expectedTitle, IEnumerable<BeatmapSetInfo> expectedBeatmapSets, ref int totalItems) private static void assertGroup(List<CarouselItem> items, int index, string expectedTitle, IEnumerable<BeatmapSetInfo> expectedBeatmapSets, ref int totalItems)

View File

@@ -5,6 +5,7 @@ using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@@ -145,14 +146,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddUntilStep("wait for filtering", () => !Carousel.IsFiltering); AddUntilStep("wait for filtering", () => !Carousel.IsFiltering);
} }
protected void ImportBeatmapForRuleset(int rulesetId) protected void SortBy(SortMode mode) => AddStep($"sort by {mode.GetDescription().ToLowerInvariant()}", () => Config.SetValue(OsuSetting.SongSelectSortingMode, mode));
protected void GroupBy(GroupMode mode) => AddStep($"group by {mode.GetDescription().ToLowerInvariant()}", () => Config.SetValue(OsuSetting.SongSelectGroupMode, mode));
protected void SortAndGroupBy(SortMode sort, GroupMode group)
{
AddStep($"sort by {sort.GetDescription().ToLowerInvariant()} & group by {group.GetDescription().ToLowerInvariant()}", () =>
{
Config.SetValue(OsuSetting.SongSelectSortingMode, sort);
Config.SetValue(OsuSetting.SongSelectGroupMode, group);
});
}
protected void ImportBeatmapForRuleset(params int[] rulesetIds)
{ {
int beatmapsCount = 0; int beatmapsCount = 0;
AddStep($"import test map for ruleset {rulesetId}", () => AddStep($"import test map for ruleset {rulesetIds}", () =>
{ {
beatmapsCount = SongSelect.IsNull() ? 0 : Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count; beatmapsCount = SongSelect.IsNull() ? 0 : Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single().SetItems.Count;
Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(3, Rulesets.AvailableRulesets.Where(r => r.OnlineID == rulesetId).ToArray())); Beatmaps.Import(TestResources.CreateTestBeatmapSetInfo(3, Rulesets.AvailableRulesets.Where(r => rulesetIds.Contains(r.OnlineID)).ToArray()));
}); });
// This is specifically for cases where the add is happening post song select load. // This is specifically for cases where the add is happening post song select load.

View File

@@ -162,5 +162,23 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddAssert("statistics still visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1); AddAssert("statistics still visible", () => display.ChildrenOfType<BeatmapTitleWedge.StatisticDifficulty>().First().Parent!.Alpha == 1);
AddAssert("tiny statistics still hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0); AddAssert("tiny statistics still hidden", () => display.ChildrenOfType<GridContainer>().Last().Alpha == 0);
} }
[Test]
public void TestMaximumLength()
{
AddStep("setup auto size", () => Child = display = new BeatmapTitleWedge.DifficultyStatisticsDisplay(true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddStep("set long statistics", () => display.Statistics = new[]
{
new BeatmapTitleWedge.StatisticDifficulty.Data("Very Long Statistic 1", 0.2f, 0.2f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Very Long Statistic 2", 0.7f, 0.7f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Very Long Statistic 3", 0.4f, 0.8f, 1f),
new BeatmapTitleWedge.StatisticDifficulty.Data("Very Long Statistic 4", 0.3f, 0.3f, 1f),
});
}
} }
} }

View File

@@ -0,0 +1,253 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2
{
/// <summary>
/// The fallback behaviour guaranteed by SongSelect is that a random selection will happen in worst case scenario.
/// Every case we're testing here is expected to have a *custom behaviour* engaging and overriding this random selection fallback.
///
/// The scenarios we care abouts are:
/// - Ruleset change (select another difficulty from same set for the new ruleset, if possible).
/// - Beatmap difficulty hidden (select closest valid difficulty from same set)
/// - Beatmap set deleted (select closest valid beatmap post-deletion)
///
/// We are working with 5 sets, each with 3 difficulties (all osu! ruleset).
/// </summary>
public partial class TestSceneSongSelectCurrentSelectionInvalidated : SongSelectTestScene
{
private BeatmapInfo? selectedBeatmap => (BeatmapInfo?)Carousel.CurrentSelection;
private BeatmapSetInfo? selectedBeatmapSet => selectedBeatmap?.BeatmapSet;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
for (int i = 0; i < 5; i++)
ImportBeatmapForRuleset(0);
LoadSongSelect();
}
[Test]
public void TestRulesetChange()
{
AddStep("disable converts", () => Config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
ImportBeatmapForRuleset(0, 1);
ImportBeatmapForRuleset(0, 1);
ImportBeatmapForRuleset(0, 2);
waitForFiltering(5);
ChangeRuleset(1);
waitForFiltering(6);
BeatmapInfo? initiallySelected = null;
AddAssert("selected is taiko", () => (initiallySelected = selectedBeatmap)?.Ruleset.OnlineID, () => Is.EqualTo(1));
ChangeRuleset(0);
waitForFiltering(7);
AddAssert("selected is osu", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(0));
AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet));
ChangeRuleset(1);
waitForFiltering(8);
AddAssert("selected is taiko", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(1));
AddAssert("selected is same set as original", () => selectedBeatmap?.BeatmapSet, () => Is.EqualTo(initiallySelected!.BeatmapSet));
ChangeRuleset(2);
waitForFiltering(9);
AddAssert("selected is catch", () => selectedBeatmap?.Ruleset.OnlineID, () => Is.EqualTo(2));
AddAssert("selected is different set", () => selectedBeatmap?.BeatmapSet, () => Is.Not.EqualTo(initiallySelected!.BeatmapSet));
}
/// <summary>
/// Make sure that deleting all sets doesn't hit some weird edge case / crash.
/// </summary>
[TestCase(SortMode.Title)]
[TestCase(SortMode.Artist)]
[TestCase(SortMode.Difficulty)]
public void TestDeleteAllSets(SortMode sortMode)
{
int filterCount = sortMode != SortMode.Title ? 2 : 1;
SortBy(sortMode);
waitForFiltering(filterCount);
BeatmapSetInfo deletedSet = null!;
for (int i = 0; i < 4; i++)
{
AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!));
waitForFiltering(filterCount + 1 + i);
selectionChangedFrom(() => deletedSet);
}
// The carousel still holds an invalid selection after the final deletion. Probably fine?
AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!));
AddUntilStep("wait for no global selection", () => Beatmap.IsDefault, () => Is.True);
}
[Test]
public void DifficultiesGrouped_DeleteSet_SelectsAdjacent()
{
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
waitForFiltering(2);
makePanelSelected<PanelGroupStarDifficulty>(2);
makePanelSelected<PanelBeatmapStandalone>(3);
// Deleting second-last, should select last
BeatmapSetInfo deletedSet = null!;
AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!));
waitForFiltering(3);
selectionChangedFrom(() => deletedSet);
assertPanelSelected<PanelBeatmapStandalone>(3);
// Deleting last, should select previous
AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!));
waitForFiltering(4);
selectionChangedFrom(() => deletedSet);
assertPanelSelected<PanelBeatmapStandalone>(2);
}
[TestCase(SortMode.Title)]
[TestCase(SortMode.Artist)]
public void SetsGrouped_DeleteSet_SelectsAdjacent(SortMode sortMode)
{
int filterCount = sortMode != SortMode.Title ? 2 : 1;
SortBy(sortMode);
waitForFiltering(filterCount);
makePanelSelected<PanelBeatmapSet>(3);
// Deleting second-last, should select last
BeatmapSetInfo deletedSet = null!;
AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!));
waitForFiltering(filterCount + 1);
selectionChangedFrom(() => deletedSet);
assertPanelSelected<PanelBeatmapSet>(3);
assertPanelSelected<PanelBeatmap>(0);
// Deleting last, should select previous
AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!));
waitForFiltering(filterCount + 2);
selectionChangedFrom(() => deletedSet);
assertPanelSelected<PanelBeatmapSet>(2);
assertPanelSelected<PanelBeatmap>(0);
}
// Same scenario as the test case above, but where selected difficulty before deletion is not first index in the expanded set.
// Basically ensures that the reselection is running `RequestRecommendedSelection` and not just relying on indices.
[TestCase(SortMode.Title)]
[TestCase(SortMode.Artist)]
public void SetsGrouped_DeleteSet_SelectsNextSetRecommendedDifficulty(SortMode sortMode)
{
int filterCount = sortMode != SortMode.Title ? 2 : 1;
SortBy(sortMode);
waitForFiltering(filterCount);
makePanelSelected<PanelBeatmapSet>(2);
makePanelSelected<PanelBeatmap>(2);
AddUntilStep("wait for beatmap to be selected", () => selectedBeatmapSet != null);
BeatmapSetInfo deletedSet = null!;
AddStep("delete selected", () => Beatmaps.Delete(deletedSet = selectedBeatmapSet!));
waitForFiltering(++filterCount);
selectionChangedFrom(() => deletedSet);
assertPanelSelected<PanelBeatmapSet>(2);
assertPanelSelected<PanelBeatmap>(0);
}
[Test]
public void TestHideBeatmap()
{
makePanelSelected<PanelBeatmapSet>(2);
makePanelSelected<PanelBeatmap>(1);
BeatmapInfo hiddenBeatmap = null!;
AddStep("hide selected", () => Beatmaps.Hide(hiddenBeatmap = selectedBeatmap!));
waitForFiltering(2);
AddAssert("selected beatmap below", () => selectedBeatmap!.BeatmapSet, () => Is.EqualTo(hiddenBeatmap.BeatmapSet));
AddStep("hide selected", () => Beatmaps.Hide(hiddenBeatmap = selectedBeatmap!));
waitForFiltering(3);
AddAssert("selected beatmap below", () => selectedBeatmap!.BeatmapSet, () => Is.EqualTo(hiddenBeatmap.BeatmapSet));
assertPanelSelected<PanelBeatmap>(0);
}
private void waitForFiltering(int filterCount = 1)
{
AddUntilStep("wait for filter count", () => Carousel.FilterCount, () => Is.EqualTo(filterCount));
AddUntilStep("filtering finished", () => Carousel.IsFiltering, () => Is.False);
}
private void makePanelSelected<T>(int index)
where T : Panel
{
AddStep($"click panel at index {index} if not selected", () =>
{
var panel = allPanels<T>().ElementAt(index).ChildrenOfType<Panel>().Single();
// May have already been selected randomly. Don't click a second time or gameplay will start.
if (!panel.Selected.Value)
panel.TriggerClick();
});
assertPanelSelected<T>(index);
}
private void selectionChangedFrom(Func<BeatmapSetInfo> deletedSet) =>
AddUntilStep("selection changed", () => selectedBeatmapSet, () => Is.Not.EqualTo(deletedSet()));
private void assertPanelSelected<T>(int index)
where T : Panel
=> AddUntilStep($"selected panel at index {index}", getActivePanelIndex<T>, () => Is.EqualTo(index));
private int getActivePanelIndex<T>()
where T : Panel
=> allPanels<T>().ToList().FindIndex(p =>
{
switch (p)
{
case PanelBeatmapStandalone pb:
return pb.Selected.Value;
case PanelBeatmap pb:
return pb.Selected.Value;
case Panel pbs:
return pbs.Expanded.Value;
default:
throw new InvalidOperationException();
}
});
private IEnumerable<T> allPanels<T>()
where T : Panel
=> Carousel.ChildrenOfType<T>().Where(p => p.Item != null).OrderBy(p => p.Y);
}
}

View File

@@ -117,14 +117,14 @@ namespace osu.Game.Tests.Visual.SongSelectV2
// TODO: old test has this step, but there doesn't seem to be any purpose for it. // TODO: old test has this step, but there doesn't seem to be any purpose for it.
// AddUntilStep("random map selected", () => Beatmap.Value != defaultBeatmap); // AddUntilStep("random map selected", () => Beatmap.Value != defaultBeatmap);
AddStep(@"Sort by Artist", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist)); SortBy(SortMode.Artist);
AddStep(@"Sort by Title", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title)); SortBy(SortMode.Title);
AddStep(@"Sort by Author", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author)); SortBy(SortMode.Author);
AddStep(@"Sort by DateAdded", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded)); SortBy(SortMode.DateAdded);
AddStep(@"Sort by BPM", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM)); SortBy(SortMode.BPM);
AddStep(@"Sort by Length", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length)); SortBy(SortMode.Length);
AddStep(@"Sort by Difficulty", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty)); SortBy(SortMode.Difficulty);
AddStep(@"Sort by Source", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source)); SortBy(SortMode.Source);
} }
[Test] [Test]
@@ -309,6 +309,24 @@ namespace osu.Game.Tests.Visual.SongSelectV2
checkMatchedBeatmaps(3); checkMatchedBeatmaps(3);
} }
[Test]
public void TestCantHideAllBeatmaps()
{
LoadSongSelect();
ImportBeatmapForRuleset(0);
checkMatchedBeatmaps(3);
AddStep("hide selected", () => Beatmaps.Hide(Beatmap.Value.BeatmapInfo));
checkMatchedBeatmaps(2);
AddStep("hide selected", () => Beatmaps.Hide(Beatmap.Value.BeatmapInfo));
checkMatchedBeatmaps(1);
AddAssert("hide fails", () => Beatmaps.Hide(Beatmap.Value.BeatmapInfo), () => Is.False);
checkMatchedBeatmaps(1);
}
private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault(); private NoResultsPlaceholder? getPlaceholder() => SongSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected)); private void checkMatchedBeatmaps(int expected) => AddUntilStep($"{expected} matching shown", () => Carousel.MatchedBeatmapsCount, () => Is.EqualTo(expected));

View File

@@ -12,6 +12,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
@@ -42,6 +43,7 @@ namespace osu.Game.Tournament.Screens.Setup
[Resolved] [Resolved]
private TournamentSceneManager? sceneManager { get; set; } private TournamentSceneManager? sceneManager { get; set; }
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
private Bindable<Size> windowSize = null!; private Bindable<Size> windowSize = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@@ -70,7 +72,8 @@ namespace osu.Game.Tournament.Screens.Setup
}, },
}; };
api.LocalUser.BindValueChanged(_ => Schedule(reload)); localUser.BindTo(api.LocalUser);
localUser.BindValueChanged(_ => Schedule(reload));
stableInfo.OnStableInfoSaved += () => Schedule(reload); stableInfo.OnStableInfoSaved += () => Schedule(reload);
reload(); reload();
} }

View File

@@ -218,24 +218,37 @@ namespace osu.Game.Beatmaps
} }
/// <summary> /// <summary>
/// Delete a beatmap difficulty. /// Hide a beatmap difficulty.
/// Will fail if all difficulties are about to be hidden.
/// </summary> /// </summary>
/// <param name="beatmapInfo">The beatmap difficulty to hide.</param> /// <param name="beatmapInfo">The beatmap difficulty to hide.</param>
public void Hide(BeatmapInfo beatmapInfo) public bool Hide(BeatmapInfo beatmapInfo)
{ {
Realm.Run(r => return Realm.Run(r =>
{ {
using (var transaction = r.BeginWrite()) using (var transaction = r.BeginWrite())
{ {
if (!beatmapInfo.IsManaged) if (!beatmapInfo.IsManaged)
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!; beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!;
if (!CanHide(beatmapInfo))
return false;
beatmapInfo.Hidden = true; beatmapInfo.Hidden = true;
transaction.Commit(); transaction.Commit();
return true;
} }
}); });
} }
public bool CanHide(BeatmapInfo beatmapInfo) => Realm.Run(r =>
{
if (!beatmapInfo.IsManaged)
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!;
return beatmapInfo.BeatmapSet!.Beatmaps.Count(b => !b.Hidden) > 1;
});
/// <summary> /// <summary>
/// Restore a beatmap difficulty. /// Restore a beatmap difficulty.
/// </summary> /// </summary>

View File

@@ -103,7 +103,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
} }
} }
public MenuItem[] ContextMenuItems => new MenuItem[] public virtual MenuItem[] ContextMenuItems => new MenuItem[]
{ {
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, Action), new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, Action),
}; };

View File

@@ -1,14 +1,18 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet;
@@ -321,5 +325,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards
buttonContainer.ShowDetails.Value = showDetails; buttonContainer.ShowDetails.Value = showDetails;
thumbnail.Dimmed.Value = showDetails; thumbnail.Dimmed.Value = showDetails;
} }
public override MenuItem[] ContextMenuItems
{
get
{
var items = base.ContextMenuItems.ToList();
foreach (var button in buttonContainer.Buttons)
{
if (button.Enabled.Value)
items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick()));
}
return items.ToArray();
}
}
} }
} }

View File

@@ -1,13 +1,17 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
@@ -165,5 +169,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards
buttonContainer.ShowDetails.Value = showDetails; buttonContainer.ShowDetails.Value = showDetails;
} }
public override MenuItem[] ContextMenuItems
{
get
{
var items = base.ContextMenuItems.ToList();
foreach (var button in buttonContainer.Buttons)
{
if (button.Enabled.Value)
items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick()));
}
return items.ToArray();
}
}
} }
} }

View File

@@ -2,14 +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.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics; using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet;
@@ -291,5 +295,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards
statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint); statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
} }
public override MenuItem[] ContextMenuItems
{
get
{
var items = base.ContextMenuItems.ToList();
foreach (var button in buttonContainer.Buttons)
{
if (button.Enabled.Value)
items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick()));
}
return items.ToArray();
}
}
} }
} }

View File

@@ -40,7 +40,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
private void updateState() private void updateState()
{ {
this.FadeTo(state.Value == DownloadState.LocallyAvailable ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint); Enabled.Value = state.Value == DownloadState.LocallyAvailable;
this.FadeTo(Enabled.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
} }
} }
} }

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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@@ -48,6 +49,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
} }
} }
public IEnumerable<BeatmapCardIconButton> Buttons => buttons;
protected override Container<Drawable> Content => mainContent; protected override Container<Drawable> Content => mainContent;
private readonly Container background; private readonly Container background;

View File

@@ -140,7 +140,7 @@ namespace osu.Game.Beatmaps.Drawables
} }
Ruleset ruleset = displayedContent.Ruleset.CreateInstance(); Ruleset ruleset = displayedContent.Ruleset.CreateInstance();
BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate); BeatmapDifficulty adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(originalDifficulty, displayedContent.Mods ?? []);
circleSize.Text = @"CS: " + adjustedDifficulty.CircleSize.ToString(@"0.##"); circleSize.Text = @"CS: " + adjustedDifficulty.CircleSize.ToString(@"0.##");
drainRate.Text = @" HP: " + adjustedDifficulty.DrainRate.ToString(@"0.##"); drainRate.Text = @" HP: " + adjustedDifficulty.DrainRate.ToString(@"0.##");

View File

@@ -102,8 +102,6 @@ namespace osu.Game.Database
/// </summary> /// </summary>
private const int schema_version = 49; private const int schema_version = 49;
public static int SchemaVersion => schema_version;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
/// </summary> /// </summary>

View File

@@ -307,7 +307,7 @@ namespace osu.Game.Graphics.Carousel
/// <summary> /// <summary>
/// Retrieve a list of all <see cref="CarouselItem"/>s currently displayed. /// Retrieve a list of all <see cref="CarouselItem"/>s currently displayed.
/// </summary> /// </summary>
public IReadOnlyCollection<CarouselItem>? GetCarouselItems() => carouselItems; public IList<CarouselItem>? GetCarouselItems() => carouselItems;
private List<CarouselItem>? carouselItems; private List<CarouselItem>? carouselItems;
@@ -691,6 +691,11 @@ namespace osu.Game.Graphics.Carousel
/// </summary> /// </summary>
protected CarouselItem? CurrentSelectionItem => currentSelection.CarouselItem; protected CarouselItem? CurrentSelectionItem => currentSelection.CarouselItem;
/// <summary>
/// The index in <see cref="GetCarouselItems"/> of the current selection, if available.
/// </summary>
protected int? CurrentSelectionIndex => currentSelection.Index;
/// <summary> /// <summary>
/// Becomes invalid when the current selection has changed and needs to be updated visually. /// Becomes invalid when the current selection has changed and needs to be updated visually.
/// </summary> /// </summary>

View File

@@ -1,8 +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.
#nullable disable
using System; using System;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@@ -14,23 +12,27 @@ namespace osu.Game.Graphics.Containers
{ {
public partial class OsuTextFlowContainer : TextFlowContainer public partial class OsuTextFlowContainer : TextFlowContainer
{ {
public OsuTextFlowContainer(Action<SpriteText> defaultCreationParameters = null) public OsuTextFlowContainer(Action<SpriteText>? defaultCreationParameters = null)
: base(defaultCreationParameters) : base(defaultCreationParameters)
{ {
} }
protected override SpriteText CreateSpriteText() => new OsuSpriteText(); protected override SpriteText CreateSpriteText() => new OsuSpriteText();
public ITextPart AddArbitraryDrawable(Drawable drawable) => AddPart(new TextPartManual(new ArbitraryDrawableWrapper { Child = drawable }.Yield())); public ITextPart AddArbitraryDrawable(Drawable drawable) => AddPart(new TextPartManual(new ArbitraryDrawableWrapper(drawable).Yield()));
public ITextPart AddIcon(IconUsage icon, Action<SpriteText> creationParameters = null) => AddText(icon.Icon.ToString(), creationParameters); public ITextPart AddIcon(IconUsage icon, Action<SpriteText>? creationParameters = null) => AddText(icon.Icon.ToString(), creationParameters);
private partial class ArbitraryDrawableWrapper : Container, IHasLineBaseHeight private partial class ArbitraryDrawableWrapper : Container, IHasLineBaseHeight
{ {
public float LineBaseHeight => (Child as IHasLineBaseHeight)?.LineBaseHeight ?? DrawHeight; private readonly IHasLineBaseHeight? lineBaseHeightSource;
public ArbitraryDrawableWrapper() public float LineBaseHeight => lineBaseHeightSource?.LineBaseHeight ?? DrawHeight;
public ArbitraryDrawableWrapper(Drawable drawable)
{ {
Child = drawable;
lineBaseHeightSource = drawable as IHasLineBaseHeight;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
} }
} }

View File

@@ -10,14 +10,14 @@ namespace osu.Game.Localisation
private const string prefix = @"osu.Game.Resources.Localisation.MenuTip"; private const string prefix = @"osu.Game.Resources.Localisation.MenuTip";
/// <summary> /// <summary>
/// "Press Ctrl-T anywhere in the game to toggle the toolbar!" /// "Press {0} anywhere in the game to toggle the toolbar!"
/// </summary> /// </summary>
public static LocalisableString ToggleToolbarShortcut => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press Ctrl-T anywhere in the game to toggle the toolbar!"); public static LocalisableString ToggleToolbarShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"toggle_toolbar_shortcut"), @"Press {0} anywhere in the game to toggle the toolbar!", keybind);
/// <summary> /// <summary>
/// "Press Ctrl-O anywhere in the game to access settings!" /// "Press {0} anywhere in the game to access settings!"
/// </summary> /// </summary>
public static LocalisableString GameSettingsShortcut => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press Ctrl-O anywhere in the game to access settings!"); public static LocalisableString GameSettingsShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"game_settings_shortcut"), @"Press {0} anywhere in the game to access settings!", keybind);
/// <summary> /// <summary>
/// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!" /// "All settings are dynamic and take effect in real-time. Try changing the skin while watching autoplay!"
@@ -40,9 +40,9 @@ namespace osu.Game.Localisation
public static LocalisableString ScreenScalingSettings => new TranslatableString(getKey(@"screen_scaling_settings"), @"Try adjusting the ""Screen Scaling"" mode to change your gameplay or UI area, even in fullscreen!"); public static LocalisableString ScreenScalingSettings => new TranslatableString(getKey(@"screen_scaling_settings"), @"Try adjusting the ""Screen Scaling"" mode to change your gameplay or UI area, even in fullscreen!");
/// <summary> /// <summary>
/// "What used to be &quot;osu!direct&quot; is available to all users just like on the website. You can access it anywhere using Ctrl-B!" /// "What used to be &quot;osu!direct&quot; is available to all users just like on the website. You can access it anywhere using {0}!"
/// </summary> /// </summary>
public static LocalisableString FreeOsuDirect => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using Ctrl-B!"); public static LocalisableString FreeOsuDirect(LocalisableString keybind) => new TranslatableString(getKey(@"free_osu_direct"), @"What used to be ""osu!direct"" is available to all users just like on the website. You can access it anywhere using {0}!", keybind);
/// <summary> /// <summary>
/// "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!" /// "Seeking in replays is available by dragging on the progress bar at the bottom of the screen or by using the left and right arrow keys!"
@@ -75,9 +75,9 @@ namespace osu.Game.Localisation
public static LocalisableString ToggleAdvancedFPSCounter => new TranslatableString(getKey(@"toggle_advanced_fps_counter"), @"Toggle advanced frame / thread statistics with Ctrl-F11!"); public static LocalisableString ToggleAdvancedFPSCounter => new TranslatableString(getKey(@"toggle_advanced_fps_counter"), @"Toggle advanced frame / thread statistics with Ctrl-F11!");
/// <summary> /// <summary>
/// "You can pause during a replay by pressing Space!" /// "You can pause during a replay by pressing {0}!"
/// </summary> /// </summary>
public static LocalisableString ReplayPausing => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing Space!"); public static LocalisableString ReplayPausing(LocalisableString keybind) => new TranslatableString(getKey(@"replay_pausing"), @"You can pause during a replay by pressing {0}!", keybind);
/// <summary> /// <summary>
/// "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!" /// "Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"
@@ -85,9 +85,9 @@ namespace osu.Game.Localisation
public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!"); public static LocalisableString ConfigurableHotkeys => new TranslatableString(getKey(@"configurable_hotkeys"), @"Most of the hotkeys in the game are configurable and can be changed to anything you want. Check the bindings panel under input settings!");
/// <summary> /// <summary>
/// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!" /// "Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via {0}!"
/// </summary> /// </summary>
public static LocalisableString SkinEditor => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via Ctrl-Shift-S!"); public static LocalisableString SkinEditor(LocalisableString keybind) => new TranslatableString(getKey(@"skin_editor"), @"Your gameplay HUD can be customised by using the skin layout editor. Open it at any time via {0}!", keybind);
/// <summary> /// <summary>
/// "You can create mod presets to make toggling your favourite mod combinations easier!" /// "You can create mod presets to make toggling your favourite mod combinations easier!"
@@ -100,14 +100,14 @@ namespace osu.Game.Localisation
public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!"); public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!");
/// <summary> /// <summary>
/// "Press Ctrl-Shift-R to switch to a random skin!" /// "Press {0} to switch to a random skin!"
/// </summary> /// </summary>
public static LocalisableString RandomSkinShortcut => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press Ctrl-Shift-R to switch to a random skin!"); public static LocalisableString RandomSkinShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press {0} to switch to a random skin!", keybind);
/// <summary> /// <summary>
/// "While watching a replay, press Ctrl-H to toggle replay settings!" /// "While watching a replay, press {0} to toggle replay settings!"
/// </summary> /// </summary>
public static LocalisableString ToggleReplaySettingsShortcut => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press Ctrl-H to toggle replay settings!"); public static LocalisableString ToggleReplaySettingsShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"toggle_replay_settings_shortcut"), @"While watching a replay, press {0} to toggle replay settings!", keybind);
/// <summary> /// <summary>
/// "You can easily copy the mods from scores on a leaderboard by right-clicking on them!" /// "You can easily copy the mods from scores on a leaderboard by right-clicking on them!"
@@ -140,9 +140,9 @@ namespace osu.Game.Localisation
public static LocalisableString GlobalStatisticsShortcut => new TranslatableString(getKey(@"global_statistics_shortcut"), @"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!"); public static LocalisableString GlobalStatisticsShortcut => new TranslatableString(getKey(@"global_statistics_shortcut"), @"Take a look under the hood at performance counters and enable verbose performance logging with Ctrl-F2!");
/// <summary> /// <summary>
/// "When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!" /// "When your gameplay HUD is hidden, you can press and hold {0} to view it temporarily!"
/// </summary> /// </summary>
public static LocalisableString PeekHUDWhenHidden => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold Ctrl to view it temporarily!"); public static LocalisableString PeekHUDWhenHidden(LocalisableString keybind) => new TranslatableString(getKey(@"peek_hud_when_hidden"), @"When your gameplay HUD is hidden, you can press and hold {0} to view it temporarily!", keybind);
/// <summary> /// <summary>
/// "Drag and drop any image into the skin editor to load it in quickly!" /// "Drag and drop any image into the skin editor to load it in quickly!"

View File

@@ -55,9 +55,29 @@ namespace osu.Game.Localisation
public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap"); public static LocalisableString EditBeatmap => new TranslatableString(getKey(@"edit_beatmap"), @"Edit beatmap");
/// <summary> /// <summary>
/// "{0} stars" /// "Circle Size"
/// </summary> /// </summary>
public static LocalisableString Stars(LocalisableString value) => new TranslatableString(getKey(@"stars"), @"{0} stars", value); public static LocalisableString CircleSize => new TranslatableString(getKey(@"circle_size"), @"Circle Size");
/// <summary>
/// "Key Count"
/// </summary>
public static LocalisableString KeyCount => new TranslatableString(getKey(@"key_count"), @"Key Count");
/// <summary>
/// "Approach Rate"
/// </summary>
public static LocalisableString ApproachRate => new TranslatableString(getKey(@"approach_rate"), @"Approach Rate");
/// <summary>
/// "Accuracy"
/// </summary>
public static LocalisableString Accuracy => new TranslatableString(getKey(@"accuracy"), @"Accuracy");
/// <summary>
/// "HP Drain"
/// </summary>
public static LocalisableString HPDrain => new TranslatableString(getKey(@"hp_drain"), @"HP Drain");
/// <summary> /// <summary>
/// "Submitted" /// "Submitted"
@@ -69,6 +89,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Ranked => new TranslatableString(getKey(@"ranked"), @"Ranked"); public static LocalisableString Ranked => new TranslatableString(getKey(@"ranked"), @"Ranked");
/// <summary>
/// "{0} stars"
/// </summary>
public static LocalisableString Stars(LocalisableString value) => new TranslatableString(getKey(@"stars"), @"{0} stars", value);
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@@ -84,11 +84,26 @@ namespace osu.Game.Online.API.Requests.Responses
/// The creator of this beatmap set. /// The creator of this beatmap set.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is not included when the set is retrieved via <see cref="SearchBeatmapSetsRequest"/>, /// This property is set differently depending on the API endpoint. When retrieved via <see cref="SearchBeatmapSetsRequest"/>,
/// but the creator's ID and username will be filled in this property from the <see cref="AuthorID"/> and <see cref="AuthorString"/> properties. /// detailed user info is not included and the creator's ID and username are filled from the <see cref="AuthorID"/> and
/// <see cref="AuthorString"/> properties. For other API endpoints, this property is set by the <see cref="author"/> setter.
/// </remarks>
public APIUser Author = new APIUser();
/// <summary>
/// Helper property to deserialize the detailed user info to <see cref="Author"/>
/// </summary>
/// <remarks>
/// This setter implements special handling for deleted users. When received a user with ID 1, it indicates
/// the original user has been deleted. In such cases, the existing <see cref="Author"/> data
/// (filled from <see cref="AuthorID"/> and <see cref="AuthorString"/>) is preserved. For valid user,
/// the provided user info replaces the existing <see cref="Author"/>.
/// </remarks> /// </remarks>
[JsonProperty(@"user")] [JsonProperty(@"user")]
public APIUser Author = new APIUser(); private APIUser author
{
set => Author = value.Id != 1 ? value : Author;
}
/// <summary> /// <summary>
/// The ID of the beatmap set's creator. /// The ID of the beatmap set's creator.

View File

@@ -22,7 +22,8 @@ namespace osu.Game.Online.API.Requests.Responses
/// </summary> /// </summary>
public const int SYSTEM_USER_ID = 0; public const int SYSTEM_USER_ID = 0;
[JsonProperty(@"id")] // In osu-web, deleted users have a null ID. When deserializing, we ignore the null value and use 1 instead.
[JsonProperty(@"id", NullValueHandling = NullValueHandling.Ignore)]
public int Id { get; set; } = 1; public int Id { get; set; } = 1;
[JsonProperty(@"join_date")] [JsonProperty(@"join_date")]

View File

@@ -9,6 +9,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Threading; using osu.Framework.Threading;
@@ -64,7 +65,6 @@ namespace osu.Game.Online.Chat
public IBindableList<Channel> AvailableChannels => availableChannels; public IBindableList<Channel> AvailableChannels => availableChannels;
private readonly IAPIProvider api; private readonly IAPIProvider api;
private readonly IChatClient chatClient;
[Resolved] [Resolved]
private UserLookupCache users { get; set; } private UserLookupCache users { get; set; }
@@ -72,6 +72,7 @@ namespace osu.Game.Online.Chat
private readonly IBindable<APIState> apiState = new Bindable<APIState>(); private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private ScheduledDelegate scheduledAck; private ScheduledDelegate scheduledAck;
private IChatClient chatClient = null!;
private long? lastSilenceMessageId; private long? lastSilenceMessageId;
private uint? lastSilenceId; private uint? lastSilenceId;
@@ -79,14 +80,13 @@ namespace osu.Game.Online.Chat
{ {
this.api = api; this.api = api;
chatClient = api.GetChatClient();
CurrentChannel.ValueChanged += currentChannelChanged; CurrentChannel.ValueChanged += currentChannelChanged;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
chatClient = api.GetChatClient();
chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch)); chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch));
chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false)); chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs)); chatClient.NewMessages += msgs => Schedule(() => addMessages(msgs));
@@ -282,8 +282,7 @@ namespace osu.Game.Online.Chat
// Check if the user has joined the requested channel already. // Check if the user has joined the requested channel already.
// This uses the channel name for comparison as the PM user's username is unavailable after a restart. // This uses the channel name for comparison as the PM user's username is unavailable after a restart.
var privateChannel = JoinedChannels.FirstOrDefault( var privateChannel = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase));
c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase));
if (privateChannel != null) if (privateChannel != null)
{ {
@@ -645,7 +644,9 @@ namespace osu.Game.Online.Chat
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
chatClient?.Dispose();
if (chatClient.IsNotNull())
chatClient.Dispose();
} }
} }

View File

@@ -5,10 +5,12 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Users; using osu.Game.Users;
@@ -35,6 +37,8 @@ namespace osu.Game.Online
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; private IAPIProvider api { get; set; } = null!;
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
private readonly Dictionary<string, UserStatistics> statisticsCache = new Dictionary<string, UserStatistics>(); private readonly Dictionary<string, UserStatistics> statisticsCache = new Dictionary<string, UserStatistics>();
/// <summary> /// <summary>
@@ -48,7 +52,8 @@ namespace osu.Game.Online
{ {
base.LoadComplete(); base.LoadComplete();
api.LocalUser.BindValueChanged(_ => localUser.BindTo(api.LocalUser);
localUser.BindValueChanged(_ =>
{ {
// queuing up requests directly on user change is unsafe, as the API status may have not been updated yet. // queuing up requests directly on user change is unsafe, as the API status may have not been updated yet.
// schedule a frame to allow the API to be in its correct state sending requests. // schedule a frame to allow the API to be in its correct state sending requests.

View File

@@ -12,6 +12,7 @@ using osu.Framework.Logging;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Online.Metadata namespace osu.Game.Online.Metadata
@@ -116,7 +117,7 @@ namespace osu.Game.Online.Metadata
} }
if (IsWatchingUserPresence) if (IsWatchingUserPresence)
BeginWatchingUserPresenceInternal(); BeginWatchingUserPresenceInternal().FireAndForget();
if (localUser.Value is not GuestUser) if (localUser.Value is not GuestUser)
{ {

View File

@@ -151,7 +151,7 @@ namespace osu.Game.Online
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (notificationsClient.IsNotNull()) if (notificationsClient.IsNotNull())
notificationsClient.MessageReceived += notifyAboutForcedDisconnection; notificationsClient.MessageReceived -= notifyAboutForcedDisconnection;
if (spectatorClient.IsNotNull()) if (spectatorClient.IsNotNull())
spectatorClient.Disconnecting -= notifyAboutForcedDisconnection; spectatorClient.Disconnecting -= notifyAboutForcedDisconnection;

View File

@@ -15,7 +15,7 @@ namespace osu.Game.Online.Rooms
[LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesHeadToHead))] [LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesHeadToHead))]
HeadToHead, HeadToHead,
[LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesTeamVs))] [LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesTeamVersus))]
TeamVersus, TeamVersus,
} }
} }

View File

@@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Humanizer; using Humanizer;
@@ -113,6 +112,9 @@ namespace osu.Game
/// </summary> /// </summary>
public const float SCREEN_EDGE_MARGIN = 12f; public const float SCREEN_EDGE_MARGIN = 12f;
private const double general_log_debounce = 60000;
private const string tablet_log_prefix = @"[Tablet] ";
public Toolbar Toolbar { get; private set; } public Toolbar Toolbar { get; private set; }
private ChatOverlay chatOverlay; private ChatOverlay chatOverlay;
@@ -241,12 +243,26 @@ namespace osu.Game
/// </summary> /// </summary>
public virtual bool HideUnlicensedContent => false; public virtual bool HideUnlicensedContent => false;
private bool tabletLogNotifyOnWarning = true;
private bool tabletLogNotifyOnError = true;
private int generalLogRecentCount;
public OsuGame(string[] args = null) public OsuGame(string[] args = null)
{ {
this.args = args; this.args = args;
forwardGeneralLogsToNotifications(); Logger.NewEntry += forwardGeneralLogToNotifications;
forwardTabletLogsToNotifications(); Logger.NewEntry += forwardTabletLogToNotifications;
Schedule(() =>
{
ITabletHandler tablet = Host.AvailableInputHandlers.OfType<ITabletHandler>().SingleOrDefault();
tablet?.Tablet.BindValueChanged(_ =>
{
tabletLogNotifyOnWarning = true;
tabletLogNotifyOnError = true;
}, true);
});
} }
#region IOverlayManager #region IOverlayManager
@@ -347,40 +363,42 @@ namespace osu.Game
if (host.Window != null) if (host.Window != null)
{ {
host.Window.CursorState |= CursorState.Hidden; host.Window.CursorState |= CursorState.Hidden;
host.Window.DragDrop += path => host.Window.DragDrop += onWindowDragDrop;
{
// on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
{
HandleLink(path);
return;
}
lock (dragDropFiles)
{
dragDropFiles.Add(path);
Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import");
// File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
// In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
dragDropImportSchedule?.Cancel();
dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100);
}
};
} }
} }
private void handlePendingDragDropImports() private void onWindowDragDrop(string path)
{ {
// on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))
{
HandleLink(path);
return;
}
lock (dragDropFiles) lock (dragDropFiles)
{ {
Logger.Log($"Handling batch import of {dragDropFiles.Count} files"); dragDropFiles.Add(path);
string[] paths = dragDropFiles.ToArray(); Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import");
dragDropFiles.Clear();
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning); // File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
// In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
dragDropImportSchedule?.Cancel();
dragDropImportSchedule = Scheduler.AddDelayed(handlePendingDragDropImports, 100);
}
void handlePendingDragDropImports()
{
lock (dragDropFiles)
{
Logger.Log($"Handling batch import of {dragDropFiles.Count} files");
string[] paths = dragDropFiles.ToArray();
dragDropFiles.Clear();
Task.Factory.StartNew(() => Import(paths), TaskCreationOptions.LongRunning);
}
} }
} }
@@ -1009,7 +1027,14 @@ namespace osu.Game
detachedBeatmapStore?.Dispose(); detachedBeatmapStore?.Dispose();
base.Dispose(isDisposing); base.Dispose(isDisposing);
SentryLogger.Dispose(); SentryLogger.Dispose();
if (Host?.Window != null)
Host.Window.DragDrop -= onWindowDragDrop;
Logger.NewEntry -= forwardGeneralLogToNotifications;
Logger.NewEntry -= forwardTabletLogToNotifications;
} }
protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults() protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults()
@@ -1029,13 +1054,6 @@ namespace osu.Game
{ {
base.LoadComplete(); base.LoadComplete();
if (RuntimeInfo.EntryAssembly.GetCustomAttribute<OfficialBuildAttribute>() == null)
Logger.Log(NotificationsStrings.NotOfficialBuild.ToString());
// Make sure the release stream setting matches the build which was just run.
if (Enum.TryParse<ReleaseStream>(Version.Split('-').Last(), true, out var releaseStream))
LocalConfig.SetValue(OsuSetting.ReleaseStream, releaseStream);
var languages = Enum.GetValues<Language>(); var languages = Enum.GetValues<Language>();
var mappings = languages.Select(language => var mappings = languages.Select(language =>
@@ -1365,115 +1383,90 @@ namespace osu.Game
overlay.Depth = (float)-Clock.CurrentTime; overlay.Depth = (float)-Clock.CurrentTime;
} }
private void forwardGeneralLogsToNotifications() private void forwardGeneralLogToNotifications(LogEntry entry)
{ {
int recentLogCount = 0; if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return;
const double debounce = 60000; if (entry.Exception is SentryOnlyDiagnosticsException)
return;
Logger.NewEntry += entry => const int short_term_display_limit = 3;
if (generalLogRecentCount < short_term_display_limit)
{ {
if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return; Schedule(() => Notifications.Post(new SimpleErrorNotification
if (entry.Exception is SentryOnlyDiagnosticsException)
return;
const int short_term_display_limit = 3;
if (recentLogCount < short_term_display_limit)
{ {
Schedule(() => Notifications.Post(new SimpleErrorNotification Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
{ Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, }));
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), }
})); else if (generalLogRecentCount == short_term_display_limit)
} {
else if (recentLogCount == short_term_display_limit) string logFile = Logger.GetLogger(entry.Target.Value).Filename;
Schedule(() => Notifications.Post(new SimpleNotification
{ {
string logFile = Logger.GetLogger(entry.Target.Value).Filename; Icon = FontAwesome.Solid.EllipsisH,
Text = NotificationsStrings.SubsequentMessagesLogged,
Schedule(() => Notifications.Post(new SimpleNotification Activated = () =>
{ {
Icon = FontAwesome.Solid.EllipsisH, Logger.Storage.PresentFileExternally(logFile);
Text = NotificationsStrings.SubsequentMessagesLogged, return true;
Activated = () => }
{ }));
Logger.Storage.PresentFileExternally(logFile); }
return true;
}
}));
}
Interlocked.Increment(ref recentLogCount); Interlocked.Increment(ref generalLogRecentCount);
Scheduler.AddDelayed(() => Interlocked.Decrement(ref recentLogCount), debounce); Scheduler.AddDelayed(() => Interlocked.Decrement(ref generalLogRecentCount), general_log_debounce);
};
} }
private void forwardTabletLogsToNotifications() private void forwardTabletLogToNotifications(LogEntry entry)
{ {
const string tablet_prefix = @"[Tablet] "; if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_log_prefix, StringComparison.OrdinalIgnoreCase))
return;
bool notifyOnWarning = true; string message = entry.Message.Replace(tablet_log_prefix, string.Empty);
bool notifyOnError = true;
Logger.NewEntry += entry => if (entry.Level == LogLevel.Error)
{ {
if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_prefix, StringComparison.OrdinalIgnoreCase)) if (!tabletLogNotifyOnError)
return; return;
string message = entry.Message.Replace(tablet_prefix, string.Empty); tabletLogNotifyOnError = false;
if (entry.Level == LogLevel.Error) Schedule(() =>
{ {
if (!notifyOnError) Notifications.Post(new SimpleNotification
return;
notifyOnError = false;
Schedule(() =>
{ {
Notifications.Post(new SimpleNotification Text = NotificationsStrings.TabletSupportDisabledDueToError(message),
{
Text = NotificationsStrings.TabletSupportDisabledDueToError(message),
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.RedDark,
});
// We only have one tablet handler currently.
// The loop here is weakly guarding against a future where more than one is added.
// If this is ever the case, this logic needs adjustment as it should probably only
// disable the relevant tablet handler rather than all.
foreach (var tabletHandler in Host.AvailableInputHandlers.OfType<ITabletHandler>())
tabletHandler.Enabled.Value = false;
});
}
else if (notifyOnWarning)
{
Schedule(() => Notifications.Post(new SimpleNotification
{
Text = NotificationsStrings.EncounteredTabletWarning,
Icon = FontAwesome.Solid.PenSquare, Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.YellowDark, IconColour = Colours.RedDark,
Activated = () => });
{
OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn);
return true;
}
}));
notifyOnWarning = false; // We only have one tablet handler currently.
} // The loop here is weakly guarding against a future where more than one is added.
}; // If this is ever the case, this logic needs adjustment as it should probably only
// disable the relevant tablet handler rather than all.
Schedule(() => foreach (var tabletHandler in Host.AvailableInputHandlers.OfType<ITabletHandler>())
tabletHandler.Enabled.Value = false;
});
}
else if (tabletLogNotifyOnWarning)
{ {
ITabletHandler tablet = Host.AvailableInputHandlers.OfType<ITabletHandler>().SingleOrDefault(); Schedule(() => Notifications.Post(new SimpleNotification
tablet?.Tablet.BindValueChanged(_ =>
{ {
notifyOnWarning = true; Text = NotificationsStrings.EncounteredTabletWarning,
notifyOnError = true; Icon = FontAwesome.Solid.PenSquare,
}, true); IconColour = Colours.YellowDark,
}); Activated = () =>
{
OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn);
return true;
}
}));
tabletLogNotifyOnWarning = false;
}
} }
private Task asyncLoadStream; private Task asyncLoadStream;

View File

@@ -504,9 +504,10 @@ namespace osu.Game
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic"); AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
AddFont(Resources, @"Fonts/Noto/Noto-Basic"); AddFont(Resources, @"Fonts/Noto/Noto-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-Hangul"); AddFont(Resources, @"Fonts/Noto/Noto-Bopomofo");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic"); AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility"); AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto/Noto-Thai"); AddFont(Resources, @"Fonts/Noto/Noto-Thai");
AddFont(Resources, @"Fonts/Venera/Venera-Light"); AddFont(Resources, @"Fonts/Venera/Venera-Light");

View File

@@ -1,8 +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.
#nullable disable
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;
@@ -18,7 +16,7 @@ namespace osu.Game.Overlays.Changelog
{ {
public partial class ChangelogBuild : FillFlowContainer public partial class ChangelogBuild : FillFlowContainer
{ {
public Action<APIChangelogBuild> SelectBuild; public required Action<APIChangelogBuild> SelectBuild { get; init; }
protected readonly APIChangelogBuild Build; protected readonly APIChangelogBuild Build;
@@ -79,7 +77,7 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Action = () => SelectBuild?.Invoke(Build), Action = () => SelectBuild.Invoke(Build),
Child = new FillFlowContainer<SpriteText> Child = new FillFlowContainer<SpriteText>
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,

View File

@@ -1,8 +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.
#nullable disable
using System; using System;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@@ -26,7 +24,7 @@ namespace osu.Game.Overlays.Changelog
{ {
public partial class ChangelogSingleBuild : ChangelogContent public partial class ChangelogSingleBuild : ChangelogContent
{ {
private APIChangelogBuild build; private readonly APIChangelogBuild build;
public ChangelogSingleBuild(APIChangelogBuild build) public ChangelogSingleBuild(APIChangelogBuild build)
{ {
@@ -38,10 +36,12 @@ namespace osu.Game.Overlays.Changelog
{ {
bool complete = false; bool complete = false;
APIChangelogBuild? onlineBuildDetails = null;
var req = new GetChangelogBuildRequest(build.UpdateStream.Name, build.Version); var req = new GetChangelogBuildRequest(build.UpdateStream.Name, build.Version);
req.Success += res => req.Success += res =>
{ {
build = res; onlineBuildDetails = res;
complete = true; complete = true;
}; };
req.Failure += _ => complete = true; req.Failure += _ => complete = true;
@@ -59,36 +59,35 @@ namespace osu.Game.Overlays.Changelog
Thread.Sleep(10); Thread.Sleep(10);
} }
if (build != null) if (onlineBuildDetails == null) return;
CommentsContainer comments;
Children = new Drawable[]
{ {
CommentsContainer comments; new ChangelogBuildWithNavigation(onlineBuildDetails) { SelectBuild = SelectBuild },
new Box
Children = new Drawable[]
{ {
new ChangelogBuildWithNavigation(build) { SelectBuild = SelectBuild }, RelativeSizeAxes = Axes.X,
new Box Height = 2,
{ Colour = colourProvider.Background6,
RelativeSizeAxes = Axes.X, Margin = new MarginPadding { Top = 30 },
Height = 2, },
Colour = colourProvider.Background6, new ChangelogSupporterPromo
Margin = new MarginPadding { Top = 30 }, {
}, Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1,
new ChangelogSupporterPromo },
{ new Box
Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1, {
}, RelativeSizeAxes = Axes.X,
new Box Height = 2,
{ Colour = colourProvider.Background6,
RelativeSizeAxes = Axes.X, Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1,
Height = 2, },
Colour = colourProvider.Background6, comments = new CommentsContainer()
Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1, };
},
comments = new CommentsContainer()
};
comments.ShowComments(CommentableType.Build, build.Id); comments.ShowComments(CommentableType.Build, onlineBuildDetails.Id);
}
} }
public partial class ChangelogBuildWithNavigation : ChangelogBuild public partial class ChangelogBuildWithNavigation : ChangelogBuild
@@ -98,7 +97,7 @@ namespace osu.Game.Overlays.Changelog
{ {
} }
private OsuSpriteText date; private OsuSpriteText date = null!;
protected override FillFlowContainer CreateHeader() protected override FillFlowContainer CreateHeader()
{ {
@@ -144,9 +143,9 @@ namespace osu.Game.Overlays.Changelog
private partial class NavigationIconButton : IconButton private partial class NavigationIconButton : IconButton
{ {
public Action<APIChangelogBuild> SelectBuild; public required Action<APIChangelogBuild> SelectBuild { get; init; }
public NavigationIconButton(APIChangelogBuild build) public NavigationIconButton(APIChangelogBuild? build)
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;

View File

@@ -1,11 +1,8 @@
// 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.
#nullable disable
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@@ -22,7 +19,7 @@ namespace osu.Game.Overlays.FirstRunSetup
[LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))] [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))]
public partial class ScreenBehaviour : WizardScreen public partial class ScreenBehaviour : WizardScreen
{ {
private SearchContainer<SettingsSection> searchContainer; private SearchContainer<SettingsSection> searchContainer = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
@@ -91,13 +88,11 @@ namespace osu.Game.Overlays.FirstRunSetup
new GraphicsSection(), new GraphicsSection(),
new OnlineSection(), new OnlineSection(),
new MaintenanceSection(), new MaintenanceSection(),
new DebugSection()
}, },
SearchTerm = SettingsItem<bool>.CLASSIC_DEFAULT_SEARCH_TERM, SearchTerm = SettingsItem<bool>.CLASSIC_DEFAULT_SEARCH_TERM,
} }
}; };
if (DebugUtils.IsDebugBuild)
searchContainer.Add(new DebugSection());
} }
private void applyClassic() private void applyClassic()

View File

@@ -180,7 +180,7 @@ namespace osu.Game.Overlays.Mods
mod.ApplyToDifficulty(adjustedDifficulty); mod.ApplyToDifficulty(adjustedDifficulty);
Ruleset ruleset = GameRuleset.Value.CreateInstance(); Ruleset ruleset = GameRuleset.Value.CreateInstance();
adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(adjustedDifficulty, rate); adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(adjustedDifficulty, Mods.Value);
TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty); TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty);

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.Development;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@@ -20,12 +21,13 @@ namespace osu.Game.Overlays.Settings.Sections
public DebugSection() public DebugSection()
{ {
Children = new Drawable[] if (DebugUtils.IsDebugBuild)
{ {
new GeneralSettings(), Add(new GeneralSettings());
new BatchImportSettings(), Add(new BatchImportSettings());
new MemorySettings(), }
};
Add(new MemorySettings());
} }
} }
} }

View File

@@ -3,7 +3,6 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
namespace osu.Game.Overlays.Settings.Sections.DebugSettings namespace osu.Game.Overlays.Settings.Sections.DebugSettings
@@ -15,19 +14,17 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig) private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig)
{ {
Children = new Drawable[] Add(new SettingsCheckbox
{ {
new SettingsCheckbox LabelText = @"Show log overlay",
{ Current = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay)
LabelText = @"Show log overlay", });
Current = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay)
}, Add(new SettingsCheckbox
new SettingsCheckbox {
{ LabelText = @"Bypass front-to-back render pass",
LabelText = @"Bypass front-to-back render pass", Current = config.GetBindable<bool>(DebugSetting.BypassFrontToBackPass)
Current = config.GetBindable<bool>(DebugSetting.BypassFrontToBackPass) });
},
};
} }
} }
} }

View File

@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Runtime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@@ -24,73 +26,112 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
SettingsButton blockAction; SettingsButton blockAction;
SettingsButton unblockAction; SettingsButton unblockAction;
Children = new Drawable[] Add(new SettingsButton
{ {
new SettingsButton Text = @"Clear all caches",
Action = () =>
{ {
Text = @"Clear all caches", host.Collect();
Action = host.Collect
}, // host.Collect() uses GCCollectionMode.Optimized, but we should be as aggressive as possible here.
new SettingsButton GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);
}
});
SettingsEnumDropdown<GCLatencyMode> latencyModeDropdown;
Add(latencyModeDropdown = new SettingsEnumDropdown<GCLatencyMode>
{
LabelText = "GC mode",
});
latencyModeDropdown.Current.BindValueChanged(mode =>
{
Logger.Log($"Changing latency mode: {mode.NewValue}");
switch (mode.NewValue)
{ {
Text = @"Compact realm", case GCLatencyMode.Default:
Action = () => // https://github.com/ppy/osu-framework/blob/1d5301018dfed1a28702be56e1d53c4835b199f2/osu.Framework/Platform/GameHost.cs#L703
GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
break;
case GCLatencyMode.Interactive:
GCSettings.LatencyMode = System.Runtime.GCLatencyMode.Interactive;
break;
}
});
if (DebugUtils.IsDebugBuild)
{
AddRange(new Drawable[]
{
new SettingsButton
{ {
// Blocking operations implicitly causes a Compact(). Text = @"Compact realm",
using (realm.BlockAllOperations(@"compact")) Action = () =>
{ {
// Blocking operations implicitly causes a Compact().
using (realm.BlockAllOperations(@"compact"))
{
}
}
},
blockAction = new SettingsButton
{
Text = @"Block realm",
},
unblockAction = new SettingsButton
{
Text = @"Unblock realm",
}
});
blockAction.Action = () =>
{
try
{
IDisposable? token = realm.BlockAllOperations(@"maintenance");
blockAction.Enabled.Value = false;
// As a safety measure, unblock after 10 seconds.
// This is to handle the case where a dev may block, but then something on the update thread
// accesses realm and blocks for eternity.
Task.Factory.StartNew(() =>
{
Thread.Sleep(10000);
unblock();
});
unblockAction.Action = unblock;
void unblock()
{
if (token.IsNull())
return;
token.Dispose();
token = null;
Scheduler.Add(() =>
{
blockAction.Enabled.Value = true;
unblockAction.Action = null;
});
} }
} }
}, catch (Exception e)
blockAction = new SettingsButton
{
Text = @"Block realm",
},
unblockAction = new SettingsButton
{
Text = @"Unblock realm",
},
};
blockAction.Action = () =>
{
try
{
IDisposable? token = realm.BlockAllOperations(@"maintenance");
blockAction.Enabled.Value = false;
// As a safety measure, unblock after 10 seconds.
// This is to handle the case where a dev may block, but then something on the update thread
// accesses realm and blocks for eternity.
Task.Factory.StartNew(() =>
{ {
Thread.Sleep(10000); Logger.Error(e, @"Blocking realm failed");
unblock();
});
unblockAction.Action = unblock;
void unblock()
{
if (token.IsNull())
return;
token.Dispose();
token = null;
Scheduler.Add(() =>
{
blockAction.Enabled.Value = true;
unblockAction.Action = null;
});
} }
} };
catch (Exception e) }
{ }
Logger.Error(e, @"Blocking realm failed");
} private enum GCLatencyMode
}; {
Default,
Interactive,
} }
} }
} }

View File

@@ -313,11 +313,11 @@ namespace osu.Game.Overlays.Settings.Sections
base.PopIn(); base.PopIn();
} }
private void rename() => skins.CurrentSkinInfo.Value.PerformWrite(skin => private void rename()
{ {
skin.Name = textBox.Text; skins.Rename(skins.CurrentSkinInfo.Value, textBox.Text);
PopOut(); PopOut();
}); }
} }
} }
} }

View File

@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Development;
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;
@@ -31,7 +30,7 @@ namespace osu.Game.Overlays
protected override IEnumerable<SettingsSection> CreateSections() protected override IEnumerable<SettingsSection> CreateSections()
{ {
var sections = new List<SettingsSection> return new List<SettingsSection>
{ {
// This list should be kept in sync with ScreenBehaviour. // This list should be kept in sync with ScreenBehaviour.
new GeneralSection(), new GeneralSection(),
@@ -44,12 +43,8 @@ namespace osu.Game.Overlays
new GraphicsSection(), new GraphicsSection(),
new OnlineSection(), new OnlineSection(),
new MaintenanceSection(), new MaintenanceSection(),
new DebugSection()
}; };
if (DebugUtils.IsDebugBuild)
sections.Add(new DebugSection());
return sections;
} }
private readonly List<SettingsSubPanel> subPanels = new List<SettingsSubPanel>(); private readonly List<SettingsSubPanel> subPanels = new List<SettingsSubPanel>();

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