1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 14:10:33 +08:00

Merge branch 'master' into pp-dev

This commit is contained in:
Dan Balasescu
2025-07-08 20:58:22 +09:00
Unverified
29 changed files with 445 additions and 135 deletions
@@ -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;
}
}
}
@@ -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);
}
@@ -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))]
@@ -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)
{
+2 -2
View File
@@ -161,7 +161,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)
@@ -296,7 +296,7 @@ namespace osu.Game.Rulesets.Mania
case ModType.System:
return new Mod[]
{
new ModScoreV2(),
new ManiaModScoreV2(),
};
default:
@@ -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;
}
}
@@ -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;
}
}
}
}
}
}
@@ -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();
}
}
@@ -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.
+21 -1
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!";
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
const double multiplier = 1 / 1.4;
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
break;
}
}
}
}
@@ -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();
}
}
@@ -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;
void IApplicableToHitObject.ApplyToHitObject(HitObject hitObject)
{
const double multiplier = 1.4;
switch (hitObject)
{
case Note:
((ManiaHitWindows)hitObject.HitWindows).DifficultyMultiplier = multiplier;
break;
case HoldNote hold:
((ManiaHitWindows)hold.Head.HitWindows).DifficultyMultiplier = multiplier;
((ManiaHitWindows)hold.Tail.HitWindows).DifficultyMultiplier = multiplier;
break;
}
}
}
}
@@ -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.
@@ -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;
}
}
}
}
}
}
@@ -16,7 +16,84 @@ namespace osu.Game.Rulesets.Mania.Scoring
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)
@@ -53,12 +120,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)
@@ -94,7 +94,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;
double comboBasedEstimatedMissCount = calculateComboBasedEstimatedMissCount(osuAttributes);
+8
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;
}
}
}
@@ -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);
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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)
+16 -16
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!"
+5 -5
View File
@@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Mods
public override bool Ranked => UsesDefaultConfiguration;
public override bool ValidForFreestyleAsRequiredMod => true;
protected const float ADJUST_RATIO = 0.5f;
public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
const float ratio = 0.5f;
difficulty.CircleSize *= ratio;
difficulty.ApproachRate *= ratio;
difficulty.DrainRate *= ratio;
difficulty.OverallDifficulty *= ratio;
difficulty.CircleSize *= ADJUST_RATIO;
difficulty.ApproachRate *= ADJUST_RATIO;
difficulty.DrainRate *= ADJUST_RATIO;
}
}
}
-1
View File
@@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Mods
public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty)
{
difficulty.DrainRate = Math.Min(difficulty.DrainRate * ADJUST_RATIO, 10.0f);
difficulty.OverallDifficulty = Math.Min(difficulty.OverallDifficulty * ADJUST_RATIO, 10.0f);
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods
/// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active.
/// It should not be used in any real capacity going forward.
/// </remarks>
public sealed class ModScoreV2 : Mod
public class ModScoreV2 : Mod
{
public override string Name => "Score V2";
public override string Acronym => @"SV2";
+100 -33
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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -12,6 +13,8 @@ using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
using osu.Game.Localisation;
@@ -27,6 +30,9 @@ namespace osu.Game.Screens.Menu
private Bindable<bool> showMenuTips = null!;
[Resolved]
private RealmKeyBindingStore keyBindingStore { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
@@ -97,42 +103,103 @@ namespace osu.Game.Screens.Menu
.FadeOutFromOne(2000, Easing.OutQuint);
}
private const int available_tips = 29;
private LocalisableString getRandomTip()
{
LocalisableString[] tips =
{
MenuTipStrings.ToggleToolbarShortcut,
MenuTipStrings.GameSettingsShortcut,
MenuTipStrings.DynamicSettings,
MenuTipStrings.NewFeaturesAreComingOnline,
MenuTipStrings.UIScalingSettings,
MenuTipStrings.ScreenScalingSettings,
MenuTipStrings.FreeOsuDirect,
MenuTipStrings.ReplaySeeking,
MenuTipStrings.MultithreadingSupport,
MenuTipStrings.TryNewMods,
MenuTipStrings.EmbeddedWebContent,
MenuTipStrings.BeatmapRightClick,
MenuTipStrings.TemporaryDeleteOperations,
MenuTipStrings.DiscoverPlaylists,
MenuTipStrings.ToggleAdvancedFPSCounter,
MenuTipStrings.GlobalStatisticsShortcut,
MenuTipStrings.ReplayPausing,
MenuTipStrings.ConfigurableHotkeys,
MenuTipStrings.PeekHUDWhenHidden,
MenuTipStrings.SkinEditor,
MenuTipStrings.DragAndDropImageInSkinEditor,
MenuTipStrings.ModPresets,
MenuTipStrings.ModCustomisationSettings,
MenuTipStrings.RandomSkinShortcut,
MenuTipStrings.ToggleReplaySettingsShortcut,
MenuTipStrings.CopyModsFromScore,
MenuTipStrings.AutoplayBeatmapShortcut,
MenuTipStrings.LazerIsNotAWord,
MenuTipStrings.RightMouseAbsoluteScroll,
};
int tipIndex = RNG.Next(0, available_tips);
return tips[RNG.Next(0, tips.Length)];
switch (tipIndex)
{
case 0:
return MenuTipStrings.ToggleToolbarShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleToolbar).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 1:
return MenuTipStrings.GameSettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSettings).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 2:
return MenuTipStrings.DynamicSettings;
case 3:
return MenuTipStrings.NewFeaturesAreComingOnline;
case 4:
return MenuTipStrings.UIScalingSettings;
case 5:
return MenuTipStrings.ScreenScalingSettings;
case 6:
return MenuTipStrings.FreeOsuDirect(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleBeatmapListing).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 7:
return MenuTipStrings.ReplaySeeking;
case 8:
return MenuTipStrings.MultithreadingSupport;
case 9:
return MenuTipStrings.TryNewMods;
case 10:
return MenuTipStrings.EmbeddedWebContent;
case 11:
return MenuTipStrings.BeatmapRightClick;
case 12:
return MenuTipStrings.TemporaryDeleteOperations;
case 13:
return MenuTipStrings.DiscoverPlaylists;
case 14:
return MenuTipStrings.ToggleAdvancedFPSCounter;
case 15:
return MenuTipStrings.GlobalStatisticsShortcut;
case 16:
return MenuTipStrings.ReplayPausing(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.TogglePauseReplay).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 17:
return MenuTipStrings.ConfigurableHotkeys;
case 18:
return MenuTipStrings.PeekHUDWhenHidden(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.HoldForHUD).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 19:
return MenuTipStrings.SkinEditor(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleSkinEditor).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 20:
return MenuTipStrings.DragAndDropImageInSkinEditor;
case 21:
return MenuTipStrings.ModPresets;
case 22:
return MenuTipStrings.ModCustomisationSettings;
case 23:
return MenuTipStrings.RandomSkinShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.RandomSkin).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 24:
return MenuTipStrings.ToggleReplaySettingsShortcut(keyBindingStore.GetReadableKeyCombinationsFor(GlobalAction.ToggleReplaySettings).FirstOrDefault() ?? InputSettingsStrings.ActionHasNoKeyBinding);
case 25:
return MenuTipStrings.CopyModsFromScore;
case 26:
return MenuTipStrings.AutoplayBeatmapShortcut;
case 27:
return MenuTipStrings.LazerIsNotAWord;
case 28:
return MenuTipStrings.RightMouseAbsoluteScroll;
}
return string.Empty;
}
}
}
+5 -2
View File
@@ -34,6 +34,9 @@ namespace osu.Game.Screens.Select.Filter
// [Description("Favourites")]
// Favourites,
[Description("Last Played")]
LastPlayed,
[Description("Length")]
Length,
@@ -46,8 +49,8 @@ namespace osu.Game.Screens.Select.Filter
[Description("Ranked Status")]
RankedStatus,
[Description("Last Played")]
LastPlayed,
[Description("Source")]
Source,
[Description("Title")]
Title,
+25 -3
View File
@@ -822,9 +822,31 @@ namespace osu.Game.Screens.SelectV2
/// <summary>
/// Defines a grouping header for a set of carousel items.
/// </summary>
/// <param name="Order">The order of this group in the carousel, sorted using ascending order.</param>
/// <param name="Title">The title of this group.</param>
public record GroupDefinition(int Order, string Title);
public record GroupDefinition
{
/// <summary>
/// The order of this group in the carousel, sorted using ascending order.
/// </summary>
public int Order { get; }
/// <summary>
/// The title of this group.
/// </summary>
public string Title { get; }
private readonly string uncasedTitle;
public GroupDefinition(int order, string title)
{
Order = order;
Title = title;
uncasedTitle = title.ToLowerInvariant();
}
public virtual bool Equals(GroupDefinition? other) => uncasedTitle == other?.uncasedTitle;
public override int GetHashCode() => HashCode.Combine(uncasedTitle);
}
/// <summary>
/// Defines a grouping header for a set of carousel items grouped by star difficulty.
@@ -202,6 +202,9 @@ namespace osu.Game.Screens.SelectV2
return defineGroupByLength(length);
}, items);
case GroupMode.Source:
return getGroupsBy(b => defineGroupBySource(b.BeatmapSet!.Metadata.Source), items);
// TODO: need implementation
//
// case GroupMode.Collections:
@@ -225,6 +228,7 @@ namespace osu.Game.Screens.SelectV2
{
return items.GroupBy(i => getGroup((BeatmapInfo)i.Model))
.OrderBy(s => s.Key.Order)
.ThenBy(s => s.Key.Title)
.Select(g => new GroupMapping(g.Key, g.ToList()))
.ToList();
}
@@ -355,6 +359,14 @@ namespace osu.Game.Screens.SelectV2
return new GroupDefinition(11, "Over 10 minutes");
}
private GroupDefinition defineGroupBySource(string source)
{
if (string.IsNullOrEmpty(source))
return new GroupDefinition(1, "Unsourced");
return new GroupDefinition(0, source);
}
private static T? aggregateMax<T>(BeatmapInfo b, Func<BeatmapInfo, T> func)
{
var beatmaps = b.BeatmapSet!.Beatmaps.Where(bb => !bb.Hidden);