同步更新,调整资源工厂逻辑,优化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>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2024 ppy Pty Ltd</Copyright>
<Copyright>Copyright (c) 2025 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags>
</PropertyGroup>
</Project>

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

View File

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

View File

@@ -1,32 +1,32 @@
{
"solution": {
"path": "osu.sln",
"projects": [
"..\\osu-resources\\osu.Game.Resources\\osu.Game.Resources.csproj",
"osu.Desktop\\osu.Desktop.csproj",
"osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj",
"osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.csproj",
"osu.Game.Rulesets.Catch\\osu.Game.Rulesets.Catch.csproj",
"osu.Game.Rulesets.Mania.Tests\\osu.Game.Rulesets.Mania.Tests.csproj",
"osu.Game.Rulesets.Mania\\osu.Game.Rulesets.Mania.csproj",
"osu.Game.Rulesets.Osu.Tests\\osu.Game.Rulesets.Osu.Tests.csproj",
"osu.Game.Rulesets.Osu\\osu.Game.Rulesets.Osu.csproj",
"osu.Game.Rulesets.Taiko.Tests\\osu.Game.Rulesets.Taiko.Tests.csproj",
"osu.Game.Rulesets.Taiko\\osu.Game.Rulesets.Taiko.csproj",
"osu.Game.Tests\\osu.Game.Tests.csproj",
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
"osu.Game.Tournament\\osu.Game.Tournament.csproj",
"osu.Game\\osu.Game.csproj",
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj",
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
"../osu-framework/osu.Framework/osu.Framework.csproj",
"../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj"
]
}
"solution": {
"path": "osu.sln",
"projects": [
"..\\osu-framework\\osu.Framework.NativeLibs\\osu.Framework.NativeLibs.csproj",
"..\\osu-framework\\osu.Framework\\osu.Framework.csproj",
"..\\osu-resources\\osu.Game.Resources\\osu.Game.Resources.csproj",
"osu.Desktop\\osu.Desktop.csproj",
"osu.Game.Benchmarks\\osu.Game.Benchmarks.csproj",
"osu.Game.Rulesets.Catch.Tests\\osu.Game.Rulesets.Catch.Tests.csproj",
"osu.Game.Rulesets.Catch\\osu.Game.Rulesets.Catch.csproj",
"osu.Game.Rulesets.Mania.Tests\\osu.Game.Rulesets.Mania.Tests.csproj",
"osu.Game.Rulesets.Mania\\osu.Game.Rulesets.Mania.csproj",
"osu.Game.Rulesets.Osu.Tests\\osu.Game.Rulesets.Osu.Tests.csproj",
"osu.Game.Rulesets.Osu\\osu.Game.Rulesets.Osu.csproj",
"osu.Game.Rulesets.Taiko.Tests\\osu.Game.Rulesets.Taiko.Tests.csproj",
"osu.Game.Rulesets.Taiko\\osu.Game.Rulesets.Taiko.csproj",
"osu.Game.Tests\\osu.Game.Tests.csproj",
"osu.Game.Tournament.Tests\\osu.Game.Tournament.Tests.csproj",
"osu.Game.Tournament\\osu.Game.Tournament.csproj",
"osu.Game\\osu.Game.csproj",
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform.Tests\\osu.Game.Rulesets.EmptyFreeform.Tests.csproj",
"Templates\\Rulesets\\ruleset-empty\\osu.Game.Rulesets.EmptyFreeform\\osu.Game.Rulesets.EmptyFreeform.csproj",
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.csproj",
"Templates\\Rulesets\\ruleset-example\\osu.Game.Rulesets.Pippidon\\osu.Game.Rulesets.Pippidon.csproj",
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling.Tests\\osu.Game.Rulesets.EmptyScrolling.Tests.csproj",
"Templates\\Rulesets\\ruleset-scrolling-empty\\osu.Game.Rulesets.EmptyScrolling\\osu.Game.Rulesets.EmptyScrolling.csproj",
"Templates\\Rulesets\\ruleset-scrolling-example\\osu.Game.Rulesets.Pippidon.Tests\\osu.Game.Rulesets.Pippidon.Tests.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()
{
Task.Run(() => Velopack.UpdateExe.Start()).FireAndForget();
Task.Run(() => Velopack.UpdateExe.Start(waitPid: (uint)Environment.ProcessId)).FireAndForget();
return true;
}

View File

@@ -53,33 +53,44 @@ namespace osu.Desktop.Updater
return false;
}
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
try
{
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();
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 () =>

View File

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

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
namespace osu.Game.Rulesets.Catch.Tests
{
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset();
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));
}
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests
var ruleset = new CatchRuleset();
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));
}

View File

@@ -33,6 +33,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Catch
@@ -265,9 +266,10 @@ namespace osu.Game.Rulesets.Catch
}
/// <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);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, CatchHitObject.PREEMPT_MAX, CatchHitObject.PREEMPT_MID, CatchHitObject.PREEMPT_MIN);
preempt /= rate;

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Mods
@@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModEasy : ModEasyWithExtraLives
{
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);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
}

View File

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

View File

@@ -9,7 +9,6 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -521,14 +520,13 @@ namespace osu.Game.Rulesets.Mania.Tests
ScoreInfo = new ScoreInfo
{
Ruleset = CreateRuleset().RulesetInfo,
Mods = [new ModScoreV2()]
Mods = [new ManiaModScoreV2()]
}
};
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))]
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]);
}
[Ignore("Tests expected to fail until stable's detailed treatment of hit windows in mania is reproduced.")]
[TestCaseSource(nameof(score_v1_convert_test_cases))]
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]);
}
[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))]
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]);
}
[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))]
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]);
}
[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))]
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]);
}
[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))]
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;
int index = KeyFlow.IndexOf(counter);
if (ezSkinConfig.GetColumnType(StageDefinition.Columns, index) == "S1")
if (ezSkinConfig.GetColumnType(StageDefinition.Columns, index) == "S")
width *= (float)specialFactor.Value;
counter.Width = width;

View File

@@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Mania
yield return new ManiaModMirror();
if (mods.HasFlag(LegacyMods.ScoreV2))
yield return new ModScoreV2();
yield return new ManiaModScoreV2();
}
public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@@ -356,7 +356,7 @@ namespace osu.Game.Rulesets.Mania
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
new ManiaModScoreV2(),
};
default:
@@ -490,6 +490,32 @@ namespace osu.Game.Rulesets.Mania
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()
{
return new ManiaFilterCriteria();

View File

@@ -2,12 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
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.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
@@ -17,29 +15,21 @@ namespace osu.Game.Rulesets.Mania.Mods
/// <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.
/// </remarks>
public interface IManiaRateAdjustmentMod : IApplicableToDifficulty, IApplicableToHitObject
public interface IManiaRateAdjustmentMod : IApplicableToHitObject
{
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)
{
switch (hitObject)
{
case Note:
hitObject.HitWindows = HitWindows;
((ManiaHitWindows)hitObject.HitWindows).SpeedMultiplier = SpeedChange.Value;
break;
case HoldNote hold:
hold.Head.HitWindows = HitWindows;
hold.Tail.HitWindows = HitWindows;
((ManiaHitWindows)hold.Head.HitWindows).SpeedMultiplier = SpeedChange.Value;
((ManiaHitWindows)hold.Tail.HitWindows).SpeedMultiplier = SpeedChange.Value;
break;
}
}

View File

@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Mods.LAsMods
public override string Acronym => "SB";
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;

View File

@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => Name;
public abstract int KeyCount { get; }
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 void ApplyToBeatmapConverter(IBeatmapConverter beatmapConverter)

View File

@@ -1,11 +1,41 @@
// 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 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.
// 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.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
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.
// 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.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
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
// 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.

View File

@@ -2,12 +2,32 @@
// See the LICENCE file in the repository root for full licence text.
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.Objects;
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 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.
// 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.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
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.
// 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.Objects;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModHardRock : ModHardRock
public class ManiaModHardRock : ModHardRock, IApplicableToHitObject
{
public override double ScoreMultiplier => 1;
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.
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
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
// 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.

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
{
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 good_window_range = new DifficultyRange(97, 82, 67);
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 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 great;
@@ -25,16 +102,6 @@ namespace osu.Game.Rulesets.Mania.Scoring
private double meh;
private double miss;
public ManiaHitWindows()
: this(1)
{
}
public ManiaHitWindows(double multiplier)
{
this.multiplier = multiplier;
}
public override bool IsHitResultAllowed(HitResult result)
{
switch (result)
@@ -55,12 +122,44 @@ namespace osu.Game.Rulesets.Mania.Scoring
public override void SetDifficulty(double difficulty)
{
perfect = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) * multiplier) + 0.5;
great = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, great_window_range) * multiplier) + 0.5;
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;
miss = Math.Floor(IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range) * multiplier) + 0.5;
overallDifficulty = difficulty;
updateWindows();
}
private void updateWindows()
{
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)

View File

@@ -2,44 +2,29 @@
// 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.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.LAsEzExtensions;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
public partial class EzHitExplosion : CompositeDrawable, IHitExplosion
public partial class EzHitExplosion : EzNoteBase, IHitExplosion
{
private TextureAnimation? animation;
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>();
protected override bool BoolUpdateColor => false;
// 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]
private EzLocalTextureFactory factory { get; set; } = null!;
private TextureAnimation? primaryAnimation;
private TextureAnimation? goodAnimation;
private bool animationsCreated;
public EzHitExplosion()
{
RelativeSizeAxes = Axes.Both;
@@ -51,78 +36,54 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
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);
OnDrawableChanged();
}
protected override void LoadComplete()
{
base.LoadComplete();
onSkinChanged();
factory.OnNoteChanged += onSkinChanged;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing) { factory.OnNoteChanged -= onSkinChanged; }
}
private void loadAnimation()
protected override void OnDrawableChanged()
{
ClearInternal();
primaryAnimation = null;
goodAnimation = null;
animation = factory.CreateAnimation("noteflare");
animationP = factory.CreateAnimation("noteflaregood");
primaryAnimation = factory.CreateAnimation("noteflare");
goodAnimation = factory.CreateAnimation("noteflaregood");
container = new Container
if (primaryAnimation?.FrameCount > 0)
AddInternal(primaryAnimation);
if (goodAnimation?.FrameCount > 0)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.None,
Children = new Drawable[]
{ animation },
};
goodAnimation.Alpha = 0;
AddInternal(goodAnimation);
}
AddInternal(container);
updateY();
animationsCreated = true;
}
private void updateY()
protected override void UpdateSize()
{
bool isSpecialColumn = ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) == "S1";
double columnWidth = columnWidthBindable.Value * (isSpecialColumn ? specialFactorBindable.Value : 1);
bool isSquare = factory.IsSquareNote("whitenote");
float aspectRatio = factory.GetRatio("whitenote");
float moveY = isSquare
? (float)columnWidth / 2 * aspectRatio
: (float)noteHeightBindable.Value / 2 * aspectRatio;
base.UpdateSize();
float moveY = NoteSize.Value.Y / 2;
// baseYPosition = LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION - (float)hitPosition.Value - moveY;
Position = new Vector2(0, -moveY);
}
private void onSkinChanged()
{
loadAnimation();
}
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
{
// private IBindable<double> hitPosition = new Bindable<double>();
protected override bool ShowSeparators => false;
protected override bool BoolUpdateColor => false;
protected override bool UseColorization => false;
protected override bool ShowSeparators => false;
protected override string ColorPrefix => "white";
[Resolved]
@@ -23,9 +23,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
[Resolved]
private IGameplayClock gameplayClock { get; set; } = null!;
// [Resolved]
// private EzSkinSettingsManager ezSkinConfig { get; set; } = null!;
public EzHitTarget()
{
RelativeSizeAxes = Axes.X;
@@ -38,11 +35,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
// hitPosition = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
// hitPosition.BindValueChanged(_ => updateY(), true);
}
// private float baseYPosition = 0f;
private double beatInterval;
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);
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.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
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
{
public partial class EzHoldNoteHead : CompositeDrawable
public partial class EzHoldNoteHead : EzNoteBase
{
private Bindable<bool> enabledColor = null!;
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!;
protected override bool ShowSeparators => true;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
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);
ezSkinConfig.OnSettingsChanged += OnConfigChanged;
factory.OnNoteChanged += OnSkinChanged;
}
var animation = Factory.CreateAnimation(newComponentName);
protected override void Dispose(bool isDisposing)
{
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)
if (animation is TextureAnimation textureAnimation && textureAnimation.FrameCount == 0)
{
animation.Dispose();
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;
animation = Factory.CreateAnimation($"{ColorPrefix}note");
OnConfigChanged();
AddInternal(container);
if (animation is TextureAnimation newTexture && newTexture.FrameCount == 0)
{
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
{
OnConfigChanged();
AddInternal(animation);
if (MainContainer != null)
{
MainContainer.Clear();
MainContainer.Child = animation;
}
}
OnConfigChanged();
Schedule(UpdateSize);
}
private void updateSizes()
protected override void UpdateSize()
{
bool isSquare = factory.IsSquareNote("whitenote");
float noteHeight = isSquare
? DrawWidth
: (float)(ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight).Value);
base.UpdateSize();
float v = NoteSize.Value.Y;
Height = v;
Height = noteHeight;
if (container.Children.Count > 0 && container.Child is Container c)
if (MainContainer?.Children.Count > 0 && MainContainer.Child is Container c)
{
container.Height = noteHeight / 2;
c.Height = noteHeight;
MainContainer.Height = v / 2;
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.Graphics;
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 osu.Game.Skinning;
using osuTK;
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>();
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>();
// 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]
private EzLocalTextureFactory factory { get; set; } = null!;
@@ -46,31 +27,20 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.Centre;
// AutoSizeAxes = Axes.Both;
RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive;
}
[BackgroundDependencyLoader]
private void load()
{
noteHeightBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight);
columnWidthBindable = ezSkinConfig.GetBindable<double>(EzSkinSetting.ColumnWidth);
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);
HitPosition = EZSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
HitPosition.BindValueChanged(_ => UpdateSize(), true);
}
protected override void LoadComplete()
{
base.LoadComplete();
onSkinChanged();
factory.OnNoteChanged += onSkinChanged;
HitPosition.BindValueChanged(pos => Y =
LegacyManiaSkinConfiguration.DEFAULT_HIT_POSITION - (float)pos.NewValue, true);
IsHitting.BindValueChanged(hitting =>
{
ClearTransforms();
@@ -89,62 +59,48 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
}, true);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
factory.OnNoteChanged -= onSkinChanged;
}
}
public void Recycle()
{
ClearTransforms();
Alpha = 0;
}
private void loadAnimation()
protected override void OnDrawableChanged()
{
ClearInternal();
animation = factory.CreateAnimation("longnoteflare");
string[] componentsToTry = { "longnoteflare", "noteflaregood", "noteflare" };
if (animation.FrameCount == 0)
foreach (string component in componentsToTry)
{
animation.Dispose();
animation = factory.CreateAnimation("noteflaregood");
animation = factory.CreateAnimation(component);
if (animation.FrameCount == 0)
if (animation.FrameCount > 0)
{
animation.Dispose();
animation = factory.CreateAnimation("noteflare");
animation.Loop = true;
break;
}
}
animation.Loop = true;
AddInternal(animation);
UpdateLNsLight();
if (animation is TextureAnimation textureAnimation && textureAnimation.FrameCount == 0)
{
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";
double columnWidth = columnWidthBindable.Value * (isSpecialColumn ? specialFactorBindable.Value : 1);
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();
base.UpdateSize();
float v = NoteSize.Value.Y / 2;
Position = new Vector2(0, -(float)HitPosition.Value - v);
}
}
}

View File

@@ -5,52 +5,31 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens;
using osu.Game.Screens.LAsEzExtensions;
using osuTK;
using osuTK.Graphics;
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 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 Bindable<bool> enabledColor = null!;
private Drawable? container;
private Drawable? lightContainer;
private EzHoldNoteHittingLayer hittingLayer = null!;
private EzHoldNoteHittingLayer? hittingLayer;
[Resolved]
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()
{
RelativeSizeAxes = Axes.Both;
@@ -64,148 +43,77 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
[BackgroundDependencyLoader(true)]
private void load(DrawableHitObject drawableObject)
{
holdNote = (DrawableHoldNote)drawableObject;
var holdNote = (DrawableHoldNote)drawableObject;
isHitting.BindTo(holdNote.IsHolding);
noteHeight = ezSkinConfig.GetBindable<double>(EzSkinSetting.NonSquareNoteHeight);
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;
hitPosition = EZSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
isHitting.BindValueChanged(onIsHittingChanged, true);
OnSkinChanged();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (isDisposing)
{
factory.OnNoteChanged -= OnSkinChanged;
ezSkinConfig.OnSettingsChanged -= OnSettingsChanged;
lightContainer?.Expire();
}
}
// protected override void Update()
// {
// base.Update();
// }
private void OnSkinChanged()
{
if (lightContainer != null)
if (hittingLayer != null)
{
column.TopLevelContainer.Remove(lightContainer, false);
lightContainer.Expire();
lightContainer = null;
column.TopLevelContainer.Remove(hittingLayer, false);
hittingLayer.Expire();
hittingLayer = null;
}
loadAnimation();
hittingLayer = new EzHoldNoteHittingLayer
{
Alpha = 0,
IsHitting = { BindTarget = isHitting }
};
lightContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Alpha = 0,
Child = hittingLayer
};
hittingLayer.HitPosition.BindTo(hitPosition);
}
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;
if (isHitting.NewValue)
{
lightContainer.ClearTransforms();
hittingLayer.ClearTransforms();
if (lightContainer.Parent == null)
column.TopLevelContainer.Add(lightContainer);
if (hittingLayer.Parent == null)
column.TopLevelContainer.Add(hittingLayer);
lightContainer.FadeIn(80);
hittingLayer.FadeIn(80);
}
else
{
lightContainer.FadeOut(120)
.OnComplete(d => column.TopLevelContainer.Remove(d, false));
hittingLayer.FadeOut(120)
.OnComplete(d => column.TopLevelContainer.Remove(d, false));
}
}
protected virtual Color4 NoteColor
public void Recycle()
{
get
{
int keyMode = stageDefinition.Columns;
int columnIndex = column.Index;
return ezSkinConfig.GetColumnColor(keyMode, columnIndex);
}
ClearTransforms();
hittingLayer?.Recycle();
}
protected virtual string ColorPrefix
protected override void OnDrawableChanged()
{
get
{
if (enabledColor.Value)
return "white";
string newComponentName = $"{ColorPrefix}note";
var body = Factory.CreateAnimation($"{ColorPrefix}longnote/middle");
var tail = Factory.CreateAnimation($"{ColorPrefix}longnote/tail");
if (ezSkinConfig.GetColumnType(stageDefinition.Columns, column.Index) == "S1")
return "green";
if (body.FrameCount == 0)
body = Factory.CreateAnimation(newComponentName);
int logicalIndex = 0;
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);
}
if (tail.FrameCount == 0)
tail = Factory.CreateAnimation(newComponentName);
topContainer = new Container
{
@@ -218,67 +126,80 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Child = tailAnimation
Child = tail
}
};
middleContainer = new Container
bodyContainer = new Container
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Masking = true,
Child = middleScaleContainer = new Container
Child = bodyScaleContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = 1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = middleInnerContainer = new Container
Child = bodyInnerContainer = new Container
{
RelativeSizeAxes = Axes.X,
Child = middleAnimation
Child = body
}
}
};
container = new Container
if (MainContainer != null)
{
RelativeSizeAxes = Axes.Both,
Children = new[] { middleContainer, 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;
MainContainer.Clear();
MainContainer.Children = [bodyContainer, topContainer];
}
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
{
private EzHoldNoteHittingLayer hittingLayer = null!;
private readonly EzHoldNoteHittingLayer hittingLayer = null!;
private TextureAnimation? animation;
private Container container = null!;
private EzSkinSettingsManager ezSkinConfig = null!;
private Bindable<bool> enabledColor = null!;
private float noteSize;
private bool isSquare;
[Resolved]
private Column column { get; set; } = null!;
@@ -48,9 +46,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
RelativeSizeAxes = Axes.Both;
Alpha = 0f;
hittingLayer = new EzHoldNoteHittingLayer { RelativeSizeAxes = Axes.Both };
AddInternal(hittingLayer);
if (drawableObject != null)
{
// accentColour.BindTo(drawableObject.AccentColour);
@@ -64,13 +59,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
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()
@@ -78,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
base.LoadComplete();
loadAnimation();
factory.OnNoteChanged += onSkinChanged;
ezSkinConfig.OnSettingsChanged += onSettingsChanged;
factory.OnNoteSizeChanged += onNoteSizeChanged;
}
private void onSkinChanged()
@@ -86,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
Schedule(loadAnimation);
}
private void onSettingsChanged()
private void onNoteSizeChanged()
{
if (enabledColor.Value)
container.Colour = NoteColor;
@@ -145,7 +133,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
}
};
onSettingsChanged();
onNoteSizeChanged();
AddInternal(container);
}

View File

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

View File

@@ -2,181 +2,48 @@
// 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.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
{
public partial class EzNote : CompositeDrawable
public partial class EzNote : EzNoteBase
{
private Bindable<bool> enabledColor = null!;
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!;
protected override bool ShowSeparators => true;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
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();
OnSkinChanged();
enabledColor.BindValueChanged(_ => OnConfigChanged(), true);
nonSquareNoteHeight.BindValueChanged(_ => updateSizes(), true);
ezSkinConfig.OnSettingsChanged += OnConfigChanged;
factory.OnNoteChanged += OnSkinChanged;
}
string newComponentName = $"{ColorPrefix}note";
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
var animation = Factory.CreateAnimation(newComponentName);
if (isDisposing)
if (animation is TextureAnimation textureAnimation && textureAnimation.FrameCount == 0)
{
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}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
}
});
animation.Dispose();
UpdateColor();
return;
}
OnConfigChanged();
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(() =>
if (MainContainer != null)
{
updateSizes();
Invalidate();
});
MainContainer.Clear();
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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -11,7 +10,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Screens;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
@@ -74,7 +72,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
separator.Height = (float)noteTrackLineHeight.Value;
}
public void UpdateGlowEffect(Color4 color)
public void UpdateGlowEffect(Colour4 color)
{
separator.Colour = new ColourInfo
{

View File

@@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
hitPositon = ezSkinConfig.GetBindable<double>(EzSkinSetting.HitPosition);
columnWidth = ezSkinConfig.GetBindable<double>(EzSkinSetting.ColumnWidth);
hitPositon.BindValueChanged(_ => OnConfigChanged());
columnWidth.BindValueChanged(_ => OnConfigChanged());
hitPositon.BindValueChanged(_ => updateSizes());
columnWidth.BindValueChanged(_ => updateSizes());
OnSkinChanged();
}
@@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
// updateSizes();
// }
private void loadAnimation()
private void OnSkinChanged()
{
ClearInternal();
@@ -77,8 +77,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
Child = stageBottom
};
// sprite.Depth = float.MinValue;
OnConfigChanged();
AddInternal(sprite);
Schedule(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;
// 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:
return new EzHitExplosion();
// return HitExplosionPool.Rent();
case ManiaSkinComponents.StageBackground:
return new Ez2StageBackground();
@@ -226,7 +227,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
int columnIndex = maniaLookup.ColumnIndex ?? 0;
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 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 Bindable<double> hitPositonBindable = new Bindable<double>();
private Bindable<bool> globalHitPosition = new Bindable<bool>();
protected override Container<Drawable> Content => content;
private readonly Container content;
@@ -30,17 +32,23 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[BackgroundDependencyLoader]
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.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)

View File

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

View File

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

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests
{
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { ApproachRate = originalApproachRate };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.ApproachRate, Is.EqualTo(originalApproachRate));
}
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset();
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.OverallDifficulty, Is.EqualTo(2.22).Within(0.01));
@@ -56,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests
var ruleset = new OsuRuleset();
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.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.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests;
using osu.Game.Tests.Visual;
using osuTK;
@@ -152,6 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests
}
[Test]
[FlakyTest]
public void TestSpinPerMinuteOnRewind()
{
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;
overallDifficulty = (80 - greatHitWindow) / 6;
overallDifficulty = (79.5 - greatHitWindow) / 6;
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
if (osuAttributes.SliderCount > 0)

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
@@ -9,5 +10,12 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModEasy : ModEasyWithExtraLives
{
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);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
}

View File

@@ -12,9 +12,9 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
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

View File

@@ -40,6 +40,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu
@@ -365,9 +366,10 @@ namespace osu.Game.Rulesets.Osu
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
/// <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);
double rate = ModUtils.CalculateRateWithMods(mods);
double preempt = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.ApproachRate, OsuHitObject.PREEMPT_MAX, OsuHitObject.PREEMPT_MID, OsuHitObject.PREEMPT_MIN);
preempt /= rate;

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Mods;
namespace osu.Game.Rulesets.Taiko.Tests
{
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset();
var difficulty = new BeatmapDifficulty { OverallDifficulty = originalOverallDifficulty };
var adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(difficulty, 1);
var adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(difficulty, []);
Assert.That(adjustedDifficulty.OverallDifficulty, Is.EqualTo(originalOverallDifficulty));
}
@@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset();
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));
}
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
var ruleset = new TaikoRuleset();
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));
}

View File

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

View File

@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
base.ApplyToDifficulty(difficulty);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
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.Skinning.Default;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Taiko
{
@@ -270,9 +271,10 @@ namespace osu.Game.Rulesets.Taiko
}
/// <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);
double rate = ModUtils.CalculateRateWithMods(mods);
double greatHitWindow = IBeatmapDifficultyInfo.DifficultyRange(adjustedDifficulty.OverallDifficulty, TaikoHitWindows.GREAT_WINDOW_RANGE);
greatHitWindow /= rate;

View File

@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using ManagedBass;
using Moq;
using NUnit.Framework;
using osu.Framework.Audio.Track;
@@ -10,7 +11,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using osuTK.Audio;
namespace osu.Game.Tests.Editing.Checks
{
@@ -28,9 +31,13 @@ namespace osu.Game.Tests.Editing.Checks
{
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]
@@ -54,6 +61,14 @@ namespace osu.Game.Tests.Editing.Checks
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]
public void TestNullBitrate()
{
@@ -87,6 +102,17 @@ namespace osu.Game.Tests.Editing.Checks
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]
public void TestTooLowBitrate()
{
@@ -98,24 +124,41 @@ namespace osu.Game.Tests.Editing.Checks
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>
/// Returns the mock of the working beatmap with the given audio properties.
/// </summary>
/// <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");
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>();
mockWorkingBeatmap.SetupGet(w => w.Beatmap).Returns(beatmap);
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;
}
}

View File

@@ -136,14 +136,39 @@ namespace osu.Game.Tests.NonVisual
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
{
public override ReleaseStream? FixedReleaseStream { get; }
public bool IsPending { get; private set; }
public int Invocations { get; private set; }
public int Completions { get; private set; }
private TaskCompletionSource<bool>? pendingCheck;
public TestUpdateManager(ReleaseStream? fixedReleaseStream = null)
{
FixedReleaseStream = fixedReleaseStream;
}
protected override async Task<bool> PerformUpdateCheck(CancellationToken cancellationToken)
{
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 BeatmapSetInfo importedSet = null!;
private BeatmapSetInfo importedSet2 = null!;
private TestMultiplayerComponents multiplayerComponents = null!;
@@ -81,12 +82,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
importedSet2 = beatmaps.Import(CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet!)!.Value.Detach();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
});
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));
}
/// <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()
{
pressReadyButton();

View File

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

View File

@@ -10,6 +10,8 @@ using osu.Game.Rulesets;
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Sprites;
@@ -193,7 +195,8 @@ namespace osu.Game.Tests.Visual.Online
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);
}
@@ -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()
{
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));
}
[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)
{
AddStep("load results", () =>
@@ -359,7 +382,7 @@ namespace osu.Game.Tests.Visual.Playlists
switch (request)
{
case ShowPlaylistScoreRequest s:
if (userScore == null)
if (userScore == null || userScore.OnlineID == -1)
triggerFail(s);
else
triggerSuccess(s, () => createUserResponse(userScore));
@@ -367,7 +390,7 @@ namespace osu.Game.Tests.Visual.Playlists
break;
case ShowPlaylistUserScoreRequest u:
if (userScore == null)
if (userScore == null || userScore.OnlineID == -1)
triggerFail(u);
else
triggerSuccess(u, () => createUserResponse(userScore));

View File

@@ -333,22 +333,32 @@ namespace osu.Game.Tests.Visual.SongSelectV2
#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)
{
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);
// 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;
return await groupingFilter.Run(beatmapSets.SelectMany(s => s.Beatmaps.Select(b => new CarouselItem(b))).ToList(), CancellationToken.None);
}
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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -145,14 +146,27 @@ namespace osu.Game.Tests.Visual.SongSelectV2
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;
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;
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.

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("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.
// AddUntilStep("random map selected", () => Beatmap.Value != defaultBeatmap);
AddStep(@"Sort by Artist", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
AddStep(@"Sort by Title", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
AddStep(@"Sort by Author", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Author));
AddStep(@"Sort by DateAdded", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.DateAdded));
AddStep(@"Sort by BPM", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.BPM));
AddStep(@"Sort by Length", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Length));
AddStep(@"Sort by Difficulty", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Difficulty));
AddStep(@"Sort by Source", () => Config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Source));
SortBy(SortMode.Artist);
SortBy(SortMode.Title);
SortBy(SortMode.Author);
SortBy(SortMode.DateAdded);
SortBy(SortMode.BPM);
SortBy(SortMode.Length);
SortBy(SortMode.Difficulty);
SortBy(SortMode.Source);
}
[Test]
@@ -309,6 +309,24 @@ namespace osu.Game.Tests.Visual.SongSelectV2
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 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.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Tournament.IPC;
@@ -42,6 +43,7 @@ namespace osu.Game.Tournament.Screens.Setup
[Resolved]
private TournamentSceneManager? sceneManager { get; set; }
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
private Bindable<Size> windowSize = null!;
[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);
reload();
}

View File

@@ -218,24 +218,37 @@ namespace osu.Game.Beatmaps
}
/// <summary>
/// Delete a beatmap difficulty.
/// Hide a beatmap difficulty.
/// Will fail if all difficulties are about to be hidden.
/// </summary>
/// <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())
{
if (!beatmapInfo.IsManaged)
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!;
if (!CanHide(beatmapInfo))
return false;
beatmapInfo.Hidden = true;
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>
/// Restore a beatmap difficulty.
/// </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),
};

View File

@@ -1,14 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
@@ -321,5 +325,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards
buttonContainer.ShowDetails.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.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
@@ -165,5 +169,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards
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.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
@@ -291,5 +295,21 @@ namespace osu.Game.Beatmaps.Drawables.Cards
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()
{
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.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
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;
private readonly Container background;

View File

@@ -140,7 +140,7 @@ namespace osu.Game.Beatmaps.Drawables
}
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.##");
drainRate.Text = @" HP: " + adjustedDifficulty.DrainRate.ToString(@"0.##");

View File

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

View File

@@ -307,7 +307,7 @@ namespace osu.Game.Graphics.Carousel
/// <summary>
/// Retrieve a list of all <see cref="CarouselItem"/>s currently displayed.
/// </summary>
public IReadOnlyCollection<CarouselItem>? GetCarouselItems() => carouselItems;
public IList<CarouselItem>? GetCarouselItems() => carouselItems;
private List<CarouselItem>? carouselItems;
@@ -691,6 +691,11 @@ namespace osu.Game.Graphics.Carousel
/// </summary>
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>
/// Becomes invalid when the current selection has changed and needs to be updated visually.
/// </summary>

View File

@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
@@ -14,23 +12,27 @@ namespace osu.Game.Graphics.Containers
{
public partial class OsuTextFlowContainer : TextFlowContainer
{
public OsuTextFlowContainer(Action<SpriteText> defaultCreationParameters = null)
public OsuTextFlowContainer(Action<SpriteText>? defaultCreationParameters = null)
: base(defaultCreationParameters)
{
}
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
{
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;
}
}

View File

@@ -10,14 +10,14 @@ namespace osu.Game.Localisation
private const string prefix = @"osu.Game.Resources.Localisation.MenuTip";
/// <summary>
/// "Press Ctrl-T anywhere in the game to toggle the toolbar!"
/// "Press {0} anywhere in the game to toggle the toolbar!"
/// </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>
/// "Press Ctrl-O anywhere in the game to access settings!"
/// "Press {0} anywhere in the game to access settings!"
/// </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>
/// "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!");
/// <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>
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>
/// "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!");
/// <summary>
/// "You can pause during a replay by pressing Space!"
/// "You can pause during a replay by pressing {0}!"
/// </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>
/// "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!");
/// <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>
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>
/// "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!");
/// <summary>
/// "Press Ctrl-Shift-R to switch to a random skin!"
/// "Press {0} to switch to a random skin!"
/// </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>
/// "While watching a replay, press Ctrl-H to toggle replay settings!"
/// "While watching a replay, press {0} to toggle replay settings!"
/// </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>
/// "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!");
/// <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>
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>
/// "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");
/// <summary>
/// "{0} stars"
/// "Circle Size"
/// </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>
/// "Submitted"
@@ -69,6 +89,11 @@ namespace osu.Game.Localisation
/// </summary>
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}";
}
}

View File

@@ -84,11 +84,26 @@ namespace osu.Game.Online.API.Requests.Responses
/// The creator of this beatmap set.
/// </summary>
/// <remarks>
/// This is not included when the set is 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.
/// This property is set differently depending on the API endpoint. When retrieved via <see cref="SearchBeatmapSetsRequest"/>,
/// 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>
[JsonProperty(@"user")]
public APIUser Author = new APIUser();
private APIUser author
{
set => Author = value.Id != 1 ? value : Author;
}
/// <summary>
/// The ID of the beatmap set's creator.

View File

@@ -22,7 +22,8 @@ namespace osu.Game.Online.API.Requests.Responses
/// </summary>
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;
[JsonProperty(@"join_date")]

View File

@@ -9,6 +9,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Threading;
@@ -64,7 +65,6 @@ namespace osu.Game.Online.Chat
public IBindableList<Channel> AvailableChannels => availableChannels;
private readonly IAPIProvider api;
private readonly IChatClient chatClient;
[Resolved]
private UserLookupCache users { get; set; }
@@ -72,6 +72,7 @@ namespace osu.Game.Online.Chat
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private ScheduledDelegate scheduledAck;
private IChatClient chatClient = null!;
private long? lastSilenceMessageId;
private uint? lastSilenceId;
@@ -79,14 +80,13 @@ namespace osu.Game.Online.Chat
{
this.api = api;
chatClient = api.GetChatClient();
CurrentChannel.ValueChanged += currentChannelChanged;
}
[BackgroundDependencyLoader]
private void load()
{
chatClient = api.GetChatClient();
chatClient.ChannelJoined += ch => Schedule(() => joinChannel(ch));
chatClient.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
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.
// This uses the channel name for comparison as the PM user's username is unavailable after a restart.
var privateChannel = JoinedChannels.FirstOrDefault(
c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase));
var privateChannel = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase));
if (privateChannel != null)
{
@@ -645,7 +644,9 @@ namespace osu.Game.Online.Chat
protected override void Dispose(bool 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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Users;
@@ -35,6 +37,8 @@ namespace osu.Game.Online
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
private readonly Dictionary<string, UserStatistics> statisticsCache = new Dictionary<string, UserStatistics>();
/// <summary>
@@ -48,7 +52,8 @@ namespace osu.Game.Online
{
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.
// 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.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Users;
namespace osu.Game.Online.Metadata
@@ -116,7 +117,7 @@ namespace osu.Game.Online.Metadata
}
if (IsWatchingUserPresence)
BeginWatchingUserPresenceInternal();
BeginWatchingUserPresenceInternal().FireAndForget();
if (localUser.Value is not GuestUser)
{

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Humanizer;
@@ -113,6 +112,9 @@ namespace osu.Game
/// </summary>
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; }
private ChatOverlay chatOverlay;
@@ -241,12 +243,26 @@ namespace osu.Game
/// </summary>
public virtual bool HideUnlicensedContent => false;
private bool tabletLogNotifyOnWarning = true;
private bool tabletLogNotifyOnError = true;
private int generalLogRecentCount;
public OsuGame(string[] args = null)
{
this.args = args;
forwardGeneralLogsToNotifications();
forwardTabletLogsToNotifications();
Logger.NewEntry += forwardGeneralLogToNotifications;
Logger.NewEntry += forwardTabletLogToNotifications;
Schedule(() =>
{
ITabletHandler tablet = Host.AvailableInputHandlers.OfType<ITabletHandler>().SingleOrDefault();
tablet?.Tablet.BindValueChanged(_ =>
{
tabletLogNotifyOnWarning = true;
tabletLogNotifyOnError = true;
}, true);
});
}
#region IOverlayManager
@@ -347,40 +363,42 @@ namespace osu.Game
if (host.Window != null)
{
host.Window.CursorState |= CursorState.Hidden;
host.Window.DragDrop += path =>
{
// 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);
}
};
host.Window.DragDrop += onWindowDragDrop;
}
}
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)
{
Logger.Log($"Handling batch import of {dragDropFiles.Count} files");
dragDropFiles.Add(path);
string[] paths = dragDropFiles.ToArray();
dragDropFiles.Clear();
Logger.Log($@"Adding ""{Path.GetFileName(path)}"" for import");
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();
base.Dispose(isDisposing);
SentryLogger.Dispose();
if (Host?.Window != null)
Host.Window.DragDrop -= onWindowDragDrop;
Logger.NewEntry -= forwardGeneralLogToNotifications;
Logger.NewEntry -= forwardTabletLogToNotifications;
}
protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults()
@@ -1029,13 +1054,6 @@ namespace osu.Game
{
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 mappings = languages.Select(language =>
@@ -1365,115 +1383,90 @@ namespace osu.Game
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;
if (entry.Exception is SentryOnlyDiagnosticsException)
return;
const int short_term_display_limit = 3;
if (recentLogCount < short_term_display_limit)
Schedule(() => Notifications.Post(new SimpleErrorNotification
{
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),
}));
}
else if (recentLogCount == short_term_display_limit)
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)
{
string logFile = Logger.GetLogger(entry.Target.Value).Filename;
Schedule(() => Notifications.Post(new SimpleNotification
{
string logFile = Logger.GetLogger(entry.Target.Value).Filename;
Schedule(() => Notifications.Post(new SimpleNotification
Icon = FontAwesome.Solid.EllipsisH,
Text = NotificationsStrings.SubsequentMessagesLogged,
Activated = () =>
{
Icon = FontAwesome.Solid.EllipsisH,
Text = NotificationsStrings.SubsequentMessagesLogged,
Activated = () =>
{
Logger.Storage.PresentFileExternally(logFile);
return true;
}
}));
}
Logger.Storage.PresentFileExternally(logFile);
return true;
}
}));
}
Interlocked.Increment(ref recentLogCount);
Scheduler.AddDelayed(() => Interlocked.Decrement(ref recentLogCount), debounce);
};
Interlocked.Increment(ref generalLogRecentCount);
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;
bool notifyOnError = true;
string message = entry.Message.Replace(tablet_log_prefix, string.Empty);
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;
string message = entry.Message.Replace(tablet_prefix, string.Empty);
tabletLogNotifyOnError = false;
if (entry.Level == LogLevel.Error)
Schedule(() =>
{
if (!notifyOnError)
return;
notifyOnError = false;
Schedule(() =>
Notifications.Post(new SimpleNotification
{
Notifications.Post(new SimpleNotification
{
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,
Text = NotificationsStrings.TabletSupportDisabledDueToError(message),
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.YellowDark,
Activated = () =>
{
OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn);
return true;
}
}));
IconColour = Colours.RedDark,
});
notifyOnWarning = false;
}
};
Schedule(() =>
// 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 (tabletLogNotifyOnWarning)
{
ITabletHandler tablet = Host.AvailableInputHandlers.OfType<ITabletHandler>().SingleOrDefault();
tablet?.Tablet.BindValueChanged(_ =>
Schedule(() => Notifications.Post(new SimpleNotification
{
notifyOnWarning = true;
notifyOnError = true;
}, true);
});
Text = NotificationsStrings.EncounteredTabletWarning,
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.YellowDark,
Activated = () =>
{
OpenUrlExternally("https://opentabletdriver.net/Tablets", LinkWarnMode.NeverWarn);
return true;
}
}));
tabletLogNotifyOnWarning = false;
}
}
private Task asyncLoadStream;

View File

@@ -504,9 +504,10 @@ namespace osu.Game
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
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-Compatibility");
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -18,7 +16,7 @@ namespace osu.Game.Overlays.Changelog
{
public partial class ChangelogBuild : FillFlowContainer
{
public Action<APIChangelogBuild> SelectBuild;
public required Action<APIChangelogBuild> SelectBuild { get; init; }
protected readonly APIChangelogBuild Build;
@@ -79,7 +77,7 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Action = () => SelectBuild?.Invoke(Build),
Action = () => SelectBuild.Invoke(Build),
Child = new FillFlowContainer<SpriteText>
{
AutoSizeAxes = Axes.Both,

View File

@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Threading;
using osu.Framework.Allocation;
@@ -26,7 +24,7 @@ namespace osu.Game.Overlays.Changelog
{
public partial class ChangelogSingleBuild : ChangelogContent
{
private APIChangelogBuild build;
private readonly APIChangelogBuild build;
public ChangelogSingleBuild(APIChangelogBuild build)
{
@@ -38,10 +36,12 @@ namespace osu.Game.Overlays.Changelog
{
bool complete = false;
APIChangelogBuild? onlineBuildDetails = null;
var req = new GetChangelogBuildRequest(build.UpdateStream.Name, build.Version);
req.Success += res =>
{
build = res;
onlineBuildDetails = res;
complete = true;
};
req.Failure += _ => complete = true;
@@ -59,36 +59,35 @@ namespace osu.Game.Overlays.Changelog
Thread.Sleep(10);
}
if (build != null)
if (onlineBuildDetails == null) return;
CommentsContainer comments;
Children = new Drawable[]
{
CommentsContainer comments;
Children = new Drawable[]
new ChangelogBuildWithNavigation(onlineBuildDetails) { SelectBuild = SelectBuild },
new Box
{
new ChangelogBuildWithNavigation(build) { SelectBuild = SelectBuild },
new Box
{
RelativeSizeAxes = Axes.X,
Height = 2,
Colour = colourProvider.Background6,
Margin = new MarginPadding { Top = 30 },
},
new ChangelogSupporterPromo
{
Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1,
},
new Box
{
RelativeSizeAxes = Axes.X,
Height = 2,
Colour = colourProvider.Background6,
Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1,
},
comments = new CommentsContainer()
};
RelativeSizeAxes = Axes.X,
Height = 2,
Colour = colourProvider.Background6,
Margin = new MarginPadding { Top = 30 },
},
new ChangelogSupporterPromo
{
Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1,
},
new Box
{
RelativeSizeAxes = Axes.X,
Height = 2,
Colour = colourProvider.Background6,
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
@@ -98,7 +97,7 @@ namespace osu.Game.Overlays.Changelog
{
}
private OsuSpriteText date;
private OsuSpriteText date = null!;
protected override FillFlowContainer CreateHeader()
{
@@ -144,9 +143,9 @@ namespace osu.Game.Overlays.Changelog
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;
Origin = Anchor.Centre;

View File

@@ -1,11 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
@@ -22,7 +19,7 @@ namespace osu.Game.Overlays.FirstRunSetup
[LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))]
public partial class ScreenBehaviour : WizardScreen
{
private SearchContainer<SettingsSection> searchContainer;
private SearchContainer<SettingsSection> searchContainer = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
@@ -91,13 +88,11 @@ namespace osu.Game.Overlays.FirstRunSetup
new GraphicsSection(),
new OnlineSection(),
new MaintenanceSection(),
new DebugSection()
},
SearchTerm = SettingsItem<bool>.CLASSIC_DEFAULT_SEARCH_TERM,
}
};
if (DebugUtils.IsDebugBuild)
searchContainer.Add(new DebugSection());
}
private void applyClassic()

View File

@@ -180,7 +180,7 @@ namespace osu.Game.Overlays.Mods
mod.ApplyToDifficulty(adjustedDifficulty);
Ruleset ruleset = GameRuleset.Value.CreateInstance();
adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(adjustedDifficulty, rate);
adjustedDifficulty = ruleset.GetAdjustedDisplayDifficulty(adjustedDifficulty, Mods.Value);
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.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@@ -20,12 +21,13 @@ namespace osu.Game.Overlays.Settings.Sections
public DebugSection()
{
Children = new Drawable[]
if (DebugUtils.IsDebugBuild)
{
new GeneralSettings(),
new BatchImportSettings(),
new MemorySettings(),
};
Add(new GeneralSettings());
Add(new BatchImportSettings());
}
Add(new MemorySettings());
}
}
}

View File

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

View File

@@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@@ -24,73 +26,112 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
SettingsButton blockAction;
SettingsButton unblockAction;
Children = new Drawable[]
Add(new SettingsButton
{
new SettingsButton
Text = @"Clear all caches",
Action = () =>
{
Text = @"Clear all caches",
Action = host.Collect
},
new SettingsButton
host.Collect();
// host.Collect() uses GCCollectionMode.Optimized, but we should be as aggressive as possible here.
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",
Action = () =>
case GCLatencyMode.Default:
// 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().
using (realm.BlockAllOperations(@"compact"))
Text = @"Compact realm",
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;
});
}
}
},
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(() =>
catch (Exception e)
{
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;
});
Logger.Error(e, @"Blocking realm failed");
}
}
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();
}
private void rename() => skins.CurrentSkinInfo.Value.PerformWrite(skin =>
private void rename()
{
skin.Name = textBox.Text;
skins.Rename(skins.CurrentSkinInfo.Value, textBox.Text);
PopOut();
});
}
}
}
}

View File

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

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