1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-08 07:03:38 +08:00

Compare commits

..

11 Commits

60 changed files with 1175 additions and 280 deletions
@@ -42,29 +42,7 @@ namespace osu.Game.Benchmarks
public override void SetUp()
{
base.SetUp();
calculator = new OsuRuleset().CreateScoreMultiplierCalculator();
}
[Benchmark]
public double ViaModScoreMultiplier() => viaModScoreMultiplier(Times, Mods);
[Test]
public void ViaModScoreMultiplier([Values(100)] int times, [ValueSource(nameof(ValuesForMods))] ModTestCase mods)
=> viaModScoreMultiplier(times, mods);
private double viaModScoreMultiplier(int times, ModTestCase mods)
{
double scoreMultiplier = 1;
for (int i = 0; i < times; ++i)
{
scoreMultiplier = 1;
foreach (var mod in mods.Mods)
scoreMultiplier *= mod.ScoreMultiplier;
}
return scoreMultiplier;
calculator = new OsuRuleset().CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
}
[Benchmark]
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Rulesets;
namespace osu.Game.Rulesets.Catch.Tests
@@ -14,28 +15,141 @@ namespace osu.Game.Rulesets.Catch.Tests
{
}
[Test]
public void TestFlashlightOnNonDefaultSettings()
=> TestModCombination([new CatchModFlashlight { ComboBasedSize = { Value = false } }]);
private static readonly object[][] test_cases =
[
#region Difficulty Reduction
[Test]
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new CatchModHalfTime { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new CatchModEasy() }, 0.5],
[new Mod[] { new CatchModNoFail() }, 0.5],
[Test]
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new CatchModDaycore { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5],
[new Mod[] { new CatchModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5],
[Test]
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new CatchModDoubleTime { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5],
[new Mod[] { new CatchModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5],
[Test]
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new CatchModNightcore { SpeedChange = { Value = speedChange } }]);
#endregion
[Test]
public void TestMultiplicativeCombination()
=> TestModCombination([new CatchModHidden(), new CatchModHardRock()]);
#region Difficulty Increase
[new Mod[] { new CatchModHardRock() }, 1.12],
[new Mod[] { new CatchModSuddenDeath() }, 1],
[new Mod[] { new CatchModPerfect() }, 1],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.00],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.00],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.02],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.02],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.04],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.04],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.06],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.06],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.08],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.08],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.10],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.10],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.12],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.12],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.14],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.14],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.16],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.16],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.18],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.18],
[new Mod[] { new CatchModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.20],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.01 } } }, 1.00],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.05 } } }, 1.00],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.10 } } }, 1.02],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.15 } } }, 1.02],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.20 } } }, 1.04],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.25 } } }, 1.04],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.30 } } }, 1.06],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.35 } } }, 1.06],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.40 } } }, 1.08],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.45 } } }, 1.08],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.50 } } }, 1.10],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.55 } } }, 1.10],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.60 } } }, 1.12],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.65 } } }, 1.12],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.70 } } }, 1.14],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.75 } } }, 1.14],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.80 } } }, 1.16],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.85 } } }, 1.16],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.90 } } }, 1.18],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 1.95 } } }, 1.18],
[new Mod[] { new CatchModNightcore { SpeedChange = { Value = 2.00 } } }, 1.20],
[new Mod[] { new CatchModHidden() }, 1.06],
[new Mod[] { new CatchModFlashlight() }, 1.12],
[new Mod[] { new CatchModFlashlight { ComboBasedSize = { Value = false } } }, 1],
[new Mod[] { new ModAccuracyChallenge() }, 1],
#endregion
#region Conversion
[new Mod[] { new CatchModDifficultyAdjust() }, 0.5],
[new Mod[] { new CatchModClassic() }, 0.96],
[new Mod[] { new CatchModMirror() }, 1],
#endregion
#region Automation
[new Mod[] { new CatchModAutoplay() }, 1],
[new Mod[] { new CatchModCinema() }, 1],
[new Mod[] { new CatchModRelax() }, 0.1],
#endregion
#region Fun
[new Mod[] { new ModWindUp() }, 0.5],
[new Mod[] { new ModWindDown() }, 0.5],
[new Mod[] { new CatchModFloatingFruits() }, 1],
[new Mod[] { new CatchModMuted() }, 1],
[new Mod[] { new CatchModNoScope() }, 1],
[new Mod[] { new CatchModMovingFast() }, 1],
[new Mod[] { new CatchModSynesthesia() }, 0.8],
#endregion
#region System
[new Mod[] { new ModScoreV2() }, 1],
#endregion
#region Combinations
[new Mod[] { new CatchModHidden(), new CatchModHardRock() }, 1.06 * 1.12]
#endregion
];
[TestCaseSource(nameof(test_cases))]
public void TestMultipliers(Mod[] mods, double expectedMultiplier)
=> TestModCombination(mods, expectedMultiplier);
}
}
+1 -1
View File
@@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Catch
}
}
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new CatchScoreMultiplierCalculator();
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new CatchScoreMultiplierCalculator(context);
public override string Description => "osu!catch";
@@ -3,11 +3,16 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -22,6 +27,29 @@ namespace osu.Game.Rulesets.Catch.Edit
{
}
protected override Drawable CreateNewComboButton() => new NewComboTernaryButton
{
Current = NewCombo,
CreateIcon = () => new Container
{
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Icon = OsuIcon.EditorFruit,
Size = new Vector2(15),
},
new SpriteIcon
{
Icon = OsuIcon.EditorNewComboSparkles,
Size = new Vector2(20),
}
},
},
};
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new CatchSelectionHandler();
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
@@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static CatchScoreMultiplierCalculator()
public CatchScoreMultiplierCalculator(ScoreMultiplierContext context)
: base(context)
{
#region Difficulty Reduction
@@ -1,8 +1,13 @@
// 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 NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Rulesets;
namespace osu.Game.Rulesets.Mania.Tests
@@ -14,24 +19,205 @@ namespace osu.Game.Rulesets.Mania.Tests
{
}
[Test]
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new ManiaModHalfTime { SpeedChange = { Value = speedChange } }]);
private static readonly object[][] test_cases =
[
#region Difficulty Reduction
[Test]
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new ManiaModDaycore { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new ManiaModEasy() }, 0.5],
[new Mod[] { new ManiaModNoFail() }, 0.5],
[Test]
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new ManiaModDoubleTime { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5],
[new Mod[] { new ManiaModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5],
[Test]
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new ManiaModNightcore { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5],
[new Mod[] { new ManiaModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5],
[Test]
public void TestMultiplicativeCombination()
=> TestModCombination([new ManiaModEasy(), new ManiaModKey4()]);
[new Mod[] { new ManiaModNoRelease() }, 0.9],
#endregion
#region Difficulty Increase
[new Mod[] { new ManiaModHardRock() }, 1],
[new Mod[] { new ManiaModSuddenDeath() }, 1],
[new Mod[] { new ManiaModPerfect() }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1],
[new Mod[] { new ManiaModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.01 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.05 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.10 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.15 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.20 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.25 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.30 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.35 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.40 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.45 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.50 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.55 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.60 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.65 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.70 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.75 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.80 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.85 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.90 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 1.95 } } }, 1],
[new Mod[] { new ManiaModNightcore { SpeedChange = { Value = 2.00 } } }, 1],
[new Mod[] { new ManiaModFadeIn() }, 1],
[new Mod[] { new ManiaModHidden() }, 1],
[new Mod[] { new ManiaModCover() }, 1],
[new Mod[] { new ManiaModFlashlight() }, 1],
[new Mod[] { new ModAccuracyChallenge() }, 1],
#endregion
#region Conversion
[new Mod[] { new ManiaModRandom() }, 1],
[new Mod[] { new ManiaModDualStages() }, 1],
[new Mod[] { new ManiaModMirror() }, 1],
[new Mod[] { new ManiaModDifficultyAdjust() }, 0.5],
[new Mod[] { new ManiaModClassic() }, 0.96],
[new Mod[] { new ManiaModInvert() }, 1],
[new Mod[] { new ManiaModConstantSpeed() }, 0.9],
[new Mod[] { new ManiaModHoldOff() }, 0.9],
[new Mod[] { new ManiaModKey1() }, 0.9],
[new Mod[] { new ManiaModKey2() }, 0.9],
[new Mod[] { new ManiaModKey3() }, 0.9],
[new Mod[] { new ManiaModKey4() }, 0.9],
[new Mod[] { new ManiaModKey5() }, 0.9],
[new Mod[] { new ManiaModKey6() }, 0.9],
[new Mod[] { new ManiaModKey7() }, 0.9],
[new Mod[] { new ManiaModKey8() }, 0.9],
[new Mod[] { new ManiaModKey9() }, 0.9],
[new Mod[] { new ManiaModKey10() }, 0.9],
#endregion
#region Automation
[new Mod[] { new ManiaModAutoplay() }, 1],
[new Mod[] { new ManiaModCinema() }, 1],
#endregion
#region Fun
[new Mod[] { new ModWindUp() }, 0.5],
[new Mod[] { new ModWindDown() }, 0.5],
[new Mod[] { new ManiaModMuted() }, 1],
[new Mod[] { new ModAdaptiveSpeed() }, 0.5],
#endregion
#region System
[new Mod[] { new ManiaModScoreV2() }, 1],
#endregion
#region Combinations
[new Mod[] { new ManiaModEasy(), new ManiaModKey4() }, 0.5 * 0.9]
#endregion
];
[TestCaseSource(nameof(test_cases))]
public void TestMultipliers(Mod[] mods, double expectedMultiplier)
=> TestModCombination(mods, expectedMultiplier);
private static readonly object[][] key_mod_multiplier_test_cases =
[
// score end date, client version, expected multiplier
// scores verifiably from old clients.
[new DateTimeOffset(2024, 1, 31, 11, 0, 0, TimeSpan.Zero), "2024.130.2", 1],
[new DateTimeOffset(2024, 12, 9, 11, 0, 0, TimeSpan.Zero), "2024.1208.0", 1],
[new DateTimeOffset(2025, 6, 12, 11, 0, 0, TimeSpan.Zero), "2025.605.3", 1],
[new DateTimeOffset(2025, 6, 28, 11, 0, 0, TimeSpan.Zero), "2025.625.0-tachyon", 1],
[new DateTimeOffset(2025, 7, 11, 11, 0, 0, TimeSpan.Zero), "2025.710.0-lazer", 1],
[new DateTimeOffset(2025, 7, 15, 11, 0, 0, TimeSpan.Zero), "2025.711.0-tachyon", 1],
// scores without explicit client versions, predating the change of multiplier.
// those MUST have used the old multiplier.
[new DateTimeOffset(2024, 1, 31, 11, 0, 0, TimeSpan.Zero), "", 1],
[new DateTimeOffset(2024, 12, 9, 11, 0, 0, TimeSpan.Zero), "", 1],
[new DateTimeOffset(2025, 6, 12, 11, 0, 0, TimeSpan.Zero), "", 1],
[new DateTimeOffset(2025, 6, 28, 11, 0, 0, TimeSpan.Zero), "", 1],
[new DateTimeOffset(2025, 7, 11, 11, 0, 0, TimeSpan.Zero), "", 1],
[new DateTimeOffset(2025, 7, 15, 11, 0, 0, TimeSpan.Zero), "", 1],
// scores without explicit client versions, AFTER the change of multiplier.
// there is NO way of verifying whether these scores use the new or old multiplier, therefore GUESS that it's the new one.
// "thankfully" the window of opportunity for this occurring *should* be slim
// (from client release with new key mod multipliers on July 18, 2025
// until spectator server release which added client version writing to server-side replays on August 1, 2025).
[new DateTimeOffset(2025, 7, 19, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
[new DateTimeOffset(2025, 7, 23, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
[new DateTimeOffset(2025, 8, 19, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
[new DateTimeOffset(2026, 6, 18, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
[new DateTimeOffset(2026, 7, 18, 0, 20, 15, 0, TimeSpan.Zero), "", 0.9],
// scores verifiably from new clients.
[new DateTimeOffset(2025, 7, 19, 0, 20, 15, 0, TimeSpan.Zero), "2025.718.0-tachyon", 0.9],
[new DateTimeOffset(2025, 7, 23, 0, 20, 15, 0, TimeSpan.Zero), "2025.721.0-tachyon", 0.9],
[new DateTimeOffset(2025, 8, 19, 0, 20, 15, 0, TimeSpan.Zero), "2025.816.0-lazer", 0.9],
[new DateTimeOffset(2026, 6, 18, 0, 20, 15, 0, TimeSpan.Zero), "2026.518.0-lazer", 0.9],
[new DateTimeOffset(2026, 7, 18, 0, 20, 15, 0, TimeSpan.Zero), "2026.522.1-tachyon", 0.9],
];
[TestCaseSource(nameof(key_mod_multiplier_test_cases))]
public void TestKeyModMultiplierCompatibility(DateTimeOffset endDate, string clientVersion, double expectedMultiplier)
{
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(new ScoreInfo
{
Date = endDate,
ClientVersion = clientVersion
}));
Assert.That(calculator.CalculateFor([new ManiaModKey4()]), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
}
}
}
@@ -8,8 +8,10 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
@@ -54,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Mod = doubleTime,
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
&& Player.ScoreProcessor.Accuracy.Value == 1
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * doubleTime.ScoreMultiplier),
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_000 * new ManiaScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor([doubleTime])),
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
@@ -3,11 +3,16 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -22,6 +27,29 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
protected override Drawable CreateNewComboButton() => new NewComboTernaryButton
{
Current = NewCombo,
CreateIcon = () => new Container
{
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Icon = OsuIcon.EditorNote,
Size = new Vector2(15),
},
new SpriteIcon
{
Icon = OsuIcon.EditorNewComboSparkles,
Size = new Vector2(20),
}
},
},
};
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
{
switch (hitObject)
+1 -1
View File
@@ -307,7 +307,7 @@ namespace osu.Game.Rulesets.Mania
}
}
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new ManiaScoreMultiplierCalculator();
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new ManiaScoreMultiplierCalculator(context);
public override string Description => "osu!mania";
@@ -1,15 +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;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring
{
public class ManiaScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static ManiaScoreMultiplierCalculator()
public ManiaScoreMultiplierCalculator(ScoreMultiplierContext context)
: base(context)
{
#region Difficulty Reduction
@@ -46,16 +49,16 @@ namespace osu.Game.Rulesets.Mania.Scoring
// Invert
Single<ManiaModConstantSpeed>(hasMultiplier: 0.9);
Single<ManiaModHoldOff>(hasMultiplier: 0.9);
Single<ManiaModKey1>(hasMultiplier: 0.9);
Single<ManiaModKey2>(hasMultiplier: 0.9);
Single<ManiaModKey3>(hasMultiplier: 0.9);
Single<ManiaModKey4>(hasMultiplier: 0.9);
Single<ManiaModKey5>(hasMultiplier: 0.9);
Single<ManiaModKey6>(hasMultiplier: 0.9);
Single<ManiaModKey7>(hasMultiplier: 0.9);
Single<ManiaModKey8>(hasMultiplier: 0.9);
Single<ManiaModKey9>(hasMultiplier: 0.9);
Single<ManiaModKey10>(hasMultiplier: 0.9);
Single<ManiaModKey1>(hasMultiplier: keyModMultiplier(Context.Score));
Single<ManiaModKey2>(hasMultiplier: keyModMultiplier(Context.Score));
Single<ManiaModKey3>(hasMultiplier: keyModMultiplier(Context.Score));
Single<ManiaModKey4>(hasMultiplier: keyModMultiplier(Context.Score));
Single<ManiaModKey5>(hasMultiplier: keyModMultiplier(Context.Score));
Single<ManiaModKey6>(hasMultiplier: keyModMultiplier(Context.Score));
Single<ManiaModKey7>(hasMultiplier: keyModMultiplier(Context.Score));
Single<ManiaModKey8>(hasMultiplier: keyModMultiplier(Context.Score));
Single<ManiaModKey9>(hasMultiplier: keyModMultiplier(Context.Score));
Single<ManiaModKey10>(hasMultiplier: keyModMultiplier(Context.Score));
#endregion
@@ -95,5 +98,49 @@ namespace osu.Game.Rulesets.Mania.Scoring
else
return 0.6 + value;
}
private const double old_key_mod_multiplier = 1;
private const double new_key_mod_multiplier = 0.9;
/// <summary>
/// <para>
/// The mod multiplier was changed from 1.0x to 0.9x in https://github.com/ppy/osu/pull/30506
/// which was included in the https://osu.ppy.sh/home/changelog/tachyon/2025.718.0 release.
/// The replay version was not bumped in the change, meaning that the only usable indicator
/// of the mod multiplier changing is the client version.
/// </para>
/// <para>
/// Unfortunately not even the client version is available on server-side recorded replays
/// recorded prior to https://github.com/ppy/osu-server-spectator/pull/290,
/// which does not appear to have been deployed until August 1
/// (https://github.com/ppy/osu-server-spectator/releases/tag/2025.801.0).
/// </para>
/// </summary>
private double keyModMultiplier(ScoreInfo? scoreInfo)
{
if (scoreInfo == null)
return new_key_mod_multiplier;
string clientVersion = scoreInfo.ClientVersion;
if (!string.IsNullOrEmpty(clientVersion))
{
string[] pieces = clientVersion.Split('.');
if (int.TryParse(pieces[0], out int year) && int.TryParse(pieces[1], out int monthDay))
{
if (year < 2025 || (year == 2025 && monthDay < 718))
return old_key_mod_multiplier;
}
return new_key_mod_multiplier;
}
// Client version not available, fallback to doing the best we can with the score's timestamp.
if (scoreInfo.Date < new DateTimeOffset(2025, 7, 18, 0, 0, 0, TimeSpan.Zero))
return old_key_mod_multiplier;
return new_key_mod_multiplier;
}
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Rulesets;
@@ -14,32 +15,165 @@ namespace osu.Game.Rulesets.Osu.Tests
{
}
[Test]
public void TestFlashlightOnNonDefaultSettings()
=> TestModCombination([new OsuModFlashlight { ComboBasedSize = { Value = false } }]);
private static readonly object[][] test_cases =
[
#region Difficulty Reduction
[Test]
public void TestHiddenOnNonDefaultSettings()
=> TestModCombination([new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }]);
[new Mod[] { new OsuModEasy() }, 0.5],
[new Mod[] { new OsuModNoFail() }, 0.5],
[Test]
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new OsuModHalfTime { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5],
[new Mod[] { new OsuModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5],
[Test]
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new OsuModDaycore { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5],
[new Mod[] { new OsuModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5],
[Test]
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new OsuModDoubleTime { SpeedChange = { Value = speedChange } }]);
#endregion
[Test]
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new OsuModNightcore { SpeedChange = { Value = speedChange } }]);
#region Difficulty Increase
[Test]
public void TestMultiplicativeCombination()
=> TestModCombination([new OsuModHidden(), new OsuModHardRock()]);
[new Mod[] { new OsuModHardRock() }, 1.06],
[new Mod[] { new OsuModSuddenDeath() }, 1],
[new Mod[] { new OsuModPerfect() }, 1],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.00],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.00],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.02],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.02],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.04],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.04],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.06],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.06],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.08],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.08],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.10],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.10],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.12],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.12],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.14],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.14],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.16],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.16],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.18],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.18],
[new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.20],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.01 } } }, 1.00],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.05 } } }, 1.00],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.10 } } }, 1.02],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.15 } } }, 1.02],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.20 } } }, 1.04],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.25 } } }, 1.04],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.30 } } }, 1.06],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.35 } } }, 1.06],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.40 } } }, 1.08],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.45 } } }, 1.08],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.50 } } }, 1.10],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.55 } } }, 1.10],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.60 } } }, 1.12],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.65 } } }, 1.12],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.70 } } }, 1.14],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.75 } } }, 1.14],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.80 } } }, 1.16],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.85 } } }, 1.16],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.90 } } }, 1.18],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 1.95 } } }, 1.18],
[new Mod[] { new OsuModNightcore { SpeedChange = { Value = 2.00 } } }, 1.20],
[new Mod[] { new OsuModHidden() }, 1.06],
[new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } } }, 1],
[new Mod[] { new OsuModTraceable() }, 1],
[new Mod[] { new OsuModFlashlight() }, 1.12],
[new Mod[] { new OsuModFlashlight { ComboBasedSize = { Value = false } } }, 1],
[new Mod[] { new OsuModBlinds() }, 1.12],
[new Mod[] { new OsuModStrictTracking() }, 1],
[new Mod[] { new OsuModAccuracyChallenge() }, 1],
#endregion
#region Conversion
[new Mod[] { new OsuModTargetPractice() }, 0.1],
[new Mod[] { new OsuModDifficultyAdjust() }, 0.5],
[new Mod[] { new OsuModClassic() }, 0.96],
[new Mod[] { new OsuModRandom() }, 1],
[new Mod[] { new OsuModMirror() }, 1],
[new Mod[] { new OsuModAlternate() }, 1],
[new Mod[] { new OsuModSingleTap() }, 1],
#endregion
#region Automation
[new Mod[] { new OsuModAutoplay() }, 1],
[new Mod[] { new OsuModCinema() }, 1],
[new Mod[] { new OsuModRelax() }, 0.1],
[new Mod[] { new OsuModAutopilot() }, 0.1],
[new Mod[] { new OsuModSpunOut() }, 0.9],
#endregion
#region Fun
[new Mod[] { new OsuModTransform() }, 1],
[new Mod[] { new OsuModWiggle() }, 1],
[new Mod[] { new OsuModSpinIn() }, 1],
[new Mod[] { new OsuModGrow() }, 1],
[new Mod[] { new OsuModDeflate() }, 1],
[new Mod[] { new ModWindUp() }, 0.5],
[new Mod[] { new ModWindDown() }, 0.5],
[new Mod[] { new OsuModBarrelRoll() }, 1],
[new Mod[] { new OsuModApproachDifferent() }, 1],
[new Mod[] { new OsuModMuted() }, 1],
[new Mod[] { new OsuModNoScope() }, 1],
[new Mod[] { new OsuModMagnetised() }, 0.5],
[new Mod[] { new OsuModRepel() }, 1],
[new Mod[] { new ModAdaptiveSpeed() }, 0.5],
[new Mod[] { new OsuModFreezeFrame() }, 1],
[new Mod[] { new OsuModBubbles() }, 1],
[new Mod[] { new OsuModSynesthesia() }, 0.8],
[new Mod[] { new OsuModDepth() }, 1],
[new Mod[] { new OsuModBloom() }, 1],
#endregion
#region System
[new Mod[] { new OsuModTouchDevice() }, 1],
[new Mod[] { new ModScoreV2() }, 1],
#endregion
#region Combinations
[new Mod[] { new OsuModHidden(), new OsuModHardRock() }, 1.06 * 1.06],
#endregion
];
[TestCaseSource(nameof(test_cases))]
public void TestMultipliers(Mod[] mods, double expectedMultiplier)
=> TestModCombination(mods, expectedMultiplier);
}
}
+1 -1
View File
@@ -234,7 +234,7 @@ namespace osu.Game.Rulesets.Osu
}
}
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new OsuScoreMultiplierCalculator();
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new OsuScoreMultiplierCalculator(context);
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetOsu };
@@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static OsuScoreMultiplierCalculator()
public OsuScoreMultiplierCalculator(ScoreMultiplierContext context)
: base(context)
{
#region Difficulty Reduction
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Tests.Rulesets;
@@ -14,28 +15,143 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
}
[Test]
public void TestFlashlightOnNonDefaultSettings()
=> TestModCombination([new TaikoModFlashlight { ComboBasedSize = { Value = false } }]);
private static readonly object[][] test_cases =
[
#region Difficulty Reduction
[Test]
public void TestHalfTimeSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new TaikoModHalfTime { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new TaikoModEasy() }, 0.5],
[new Mod[] { new TaikoModNoFail() }, 0.5],
[Test]
public void TestDaycoreSpeeds([Values(0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.99)] double speedChange)
=> TestModCombination([new TaikoModDaycore { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.50 } } }, 0.1],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.55 } } }, 0.1],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.60 } } }, 0.2],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.65 } } }, 0.2],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.70 } } }, 0.3],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.75 } } }, 0.3],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.80 } } }, 0.4],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.85 } } }, 0.4],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.90 } } }, 0.5],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.95 } } }, 0.5],
[new Mod[] { new TaikoModHalfTime { SpeedChange = { Value = 0.99 } } }, 0.5],
[Test]
public void TestDoubleTimeSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new TaikoModDoubleTime { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.50 } } }, 0.1],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.55 } } }, 0.1],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.60 } } }, 0.2],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.65 } } }, 0.2],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.70 } } }, 0.3],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.75 } } }, 0.3],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.80 } } }, 0.4],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.85 } } }, 0.4],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.90 } } }, 0.5],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.95 } } }, 0.5],
[new Mod[] { new TaikoModDaycore { SpeedChange = { Value = 0.99 } } }, 0.5],
[Test]
public void TestNightcoreSpeeds([Values(1.01, 1.05, 1.1, 1.15, 1.2, 1.25, 1.3, 1.35, 1.4, 1.45, 1.5, 1.55, 1.6, 1.65, 1.7, 1.75, 1.8, 1.85, 1.9, 1.95, 2)] double speedChange)
=> TestModCombination([new TaikoModNightcore { SpeedChange = { Value = speedChange } }]);
[new Mod[] { new TaikoModSimplifiedRhythm() }, 0.6],
[Test]
public void TestMultiplicativeCombination()
=> TestModCombination([new TaikoModHidden(), new TaikoModHardRock()]);
#endregion
#region Difficulty Increase
[new Mod[] { new TaikoModHardRock() }, 1.06],
[new Mod[] { new TaikoModSuddenDeath() }, 1],
[new Mod[] { new TaikoModPerfect() }, 1],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.01 } } }, 1.00],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.05 } } }, 1.00],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.10 } } }, 1.02],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.15 } } }, 1.02],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.20 } } }, 1.04],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.25 } } }, 1.04],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.30 } } }, 1.06],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.35 } } }, 1.06],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.40 } } }, 1.08],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.45 } } }, 1.08],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.50 } } }, 1.10],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.55 } } }, 1.10],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.60 } } }, 1.12],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.65 } } }, 1.12],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.70 } } }, 1.14],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.75 } } }, 1.14],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.80 } } }, 1.16],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.85 } } }, 1.16],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.90 } } }, 1.18],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 1.95 } } }, 1.18],
[new Mod[] { new TaikoModDoubleTime { SpeedChange = { Value = 2.00 } } }, 1.20],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.01 } } }, 1.00],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.05 } } }, 1.00],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.10 } } }, 1.02],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.15 } } }, 1.02],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.20 } } }, 1.04],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.25 } } }, 1.04],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.30 } } }, 1.06],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.35 } } }, 1.06],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.40 } } }, 1.08],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.45 } } }, 1.08],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.50 } } }, 1.10],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.55 } } }, 1.10],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.60 } } }, 1.12],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.65 } } }, 1.12],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.70 } } }, 1.14],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.75 } } }, 1.14],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.80 } } }, 1.16],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.85 } } }, 1.16],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.90 } } }, 1.18],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 1.95 } } }, 1.18],
[new Mod[] { new TaikoModNightcore { SpeedChange = { Value = 2.00 } } }, 1.20],
[new Mod[] { new TaikoModHidden() }, 1.06],
[new Mod[] { new TaikoModFlashlight() }, 1.12],
[new Mod[] { new TaikoModFlashlight { ComboBasedSize = { Value = false } } }, 1],
[new Mod[] { new ModAccuracyChallenge() }, 1],
#endregion
#region Conversion
[new Mod[] { new TaikoModRandom() }, 1],
[new Mod[] { new TaikoModDifficultyAdjust() }, 0.5],
[new Mod[] { new TaikoModClassic() }, 0.96],
[new Mod[] { new TaikoModSwap() }, 1],
[new Mod[] { new TaikoModSingleTap() }, 1],
[new Mod[] { new TaikoModConstantSpeed() }, 0.9],
#endregion
#region Automation
[new Mod[] { new TaikoModAutoplay() }, 1],
[new Mod[] { new TaikoModCinema() }, 1],
[new Mod[] { new TaikoModRelax() }, 0.1],
#endregion
#region Fun
[new Mod[] { new ModWindUp() }, 0.5],
[new Mod[] { new ModWindDown() }, 0.5],
[new Mod[] { new TaikoModMuted() }, 1],
[new Mod[] { new ModAdaptiveSpeed() }, 0.5],
#endregion
#region System
[new Mod[] { new ModScoreV2() }, 1],
#endregion
#region Combinations
[new Mod[] { new TaikoModHidden(), new TaikoModHardRock() }, 1.06 * 1.06]
#endregion
];
[TestCaseSource(nameof(test_cases))]
public void TestMultipliers(Mod[] mods, double expectedMultiplier)
=> TestModCombination(mods, expectedMultiplier);
}
}
@@ -3,10 +3,15 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -21,6 +26,29 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
protected override Drawable CreateNewComboButton() => new NewComboTernaryButton
{
Current = NewCombo,
CreateIcon = () => new Container
{
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Icon = OsuIcon.EditorHit,
Size = new Vector2(15),
},
new SpriteIcon
{
Icon = OsuIcon.EditorNewComboSparkles,
Size = new Vector2(20),
}
},
},
};
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new TaikoSelectionHandler();
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) =>
@@ -9,7 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
public class TaikoScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static TaikoScoreMultiplierCalculator()
public TaikoScoreMultiplierCalculator(ScoreMultiplierContext context)
: base(context)
{
#region Difficulty Reduction
+1 -1
View File
@@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Taiko
}
}
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new TaikoScoreMultiplierCalculator();
public override ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new TaikoScoreMultiplierCalculator(context);
public override string Description => "osu!taiko";
@@ -30,6 +30,23 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode.Beatmap.BeatmapInfo.Metadata.BackgroundFile, Is.EqualTo("bg.jpg"));
}
[Test]
public void TestBackgroundOffset()
{
var initial = createComponents();
initial.Beatmap.BeatmapInfo.Metadata.BackgroundFile = "bg_offset.jpg";
initial.Storyboard.BackgroundOffset = new Vector2(0, 45);
var encoded = encode(initial);
var decodedAfterEncode = decode(encoded);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.Beatmap.BeatmapInfo.Metadata.BackgroundFile, Is.EqualTo("bg_offset.jpg"));
Assert.That(decodedAfterEncode.Storyboard.BackgroundOffset, Is.EqualTo(new Vector2(0, 45)));
});
}
[Test]
public void TestVideos()
{
@@ -122,6 +122,29 @@ namespace osu.Game.Tests.Beatmaps.IO
() => Is.EqualTo(384).Within(0.00001));
}
[Test]
public void TestBackgroundSpecificationPreserved()
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
// Ensure importer encoding is correct
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"241526 Soleily - Renatus.osz"));
AddAssert("beatmap background is correct", () => beatmap.BeatmapInfo.Metadata.BackgroundFile, () => Is.EqualTo("machinetop_background.jpg"));
// Ensure exporter legacy conversion is correct
AddStep("export", () =>
{
outStream = new MemoryStream();
new LegacyBeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("beatmap background is still correct", () => beatmap.BeatmapInfo.Metadata.BackgroundFile, () => Is.EqualTo("machinetop_background.jpg"));
}
[Test]
public void TestExportStability()
{
@@ -81,7 +81,7 @@ namespace osu.Game.Tests.Gameplay
AccuracyJudgementCount = 1,
ComboPortion = 0,
BonusPortion = 0
}, DateTimeOffset.Now)
}, DateTimeOffset.Now, [], 0, [])
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
@@ -99,7 +99,7 @@ namespace osu.Game.Tests.Gameplay
AccuracyJudgementCount = 0,
ComboPortion = 0,
BonusPortion = 0
}, DateTimeOffset.Now)
}, DateTimeOffset.Now, [], 0, [])
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
@@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Tests.Rulesets.Scoring
{
@@ -12,7 +13,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[Test]
public void TestFlatMultiplier()
{
var calculator = new TestScoreMultiplierCalculator();
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
double multiplier = calculator.CalculateFor([new OsuModEasy()]);
@@ -22,7 +23,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[Test]
public void TestSettingDependentMultiplier()
{
var calculator = new TestScoreMultiplierCalculator();
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
double multiplier = calculator.CalculateFor([new OsuModDaycore { SpeedChange = { Value = 0.6 } }]);
@@ -32,17 +33,17 @@ namespace osu.Game.Tests.Rulesets.Scoring
[Test]
public void TestContextDependentMultiplier()
{
var calculator = new TestScoreMultiplierCalculator();
TestScoreMultiplierCalculator calculator;
double multiplier;
Assert.Multiple(() =>
{
calculator.HardRockPenalty = false;
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
Assert.That(multiplier, Is.EqualTo(1.4));
calculator.HardRockPenalty = true;
calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext(new ScoreInfo { ClientVersion = "2024.123.0" }));
multiplier = calculator.CalculateFor([new OsuModHardRock()]);
Assert.That(multiplier, Is.EqualTo(1.2));
});
@@ -51,7 +52,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[Test]
public void TestCombinationMultiplier()
{
var calculator = new TestScoreMultiplierCalculator();
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
double multiplier = calculator.CalculateFor([new OsuModEasy(), new OsuModDaycore()]);
@@ -61,7 +62,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[Test]
public void TestCombinationAndFlatMultipliers()
{
var calculator = new TestScoreMultiplierCalculator();
var calculator = new TestScoreMultiplierCalculator(new ScoreMultiplierContext());
double multiplier = calculator.CalculateFor([new OsuModDaycore(), new OsuModHardRock(), new OsuModEasy()]);
@@ -70,15 +71,14 @@ namespace osu.Game.Tests.Rulesets.Scoring
private class TestScoreMultiplierCalculator : ScoreMultiplierCalculator
{
static TestScoreMultiplierCalculator()
public TestScoreMultiplierCalculator(ScoreMultiplierContext context)
: base(context)
{
Single<OsuModEasy>(hasMultiplier: 0.15);
Single<OsuModDaycore>(hasMultiplier: daycore => (1 + daycore.SpeedChange.Value) / 4);
Single<OsuModHardRock, TestScoreMultiplierCalculator>(hasMultiplier: (_, ctx) => ctx.HardRockPenalty ? 1.2 : 1.4);
Single<OsuModHardRock>(hasMultiplier: _ => context.Score?.ClientVersion == "2024.123.0" ? 1.2 : 1.4);
Combination<OsuModEasy, OsuModDaycore>(hasMultiplier: (_, _) => 0.003);
}
public bool HardRockPenalty { get; set; }
}
}
}
@@ -221,7 +221,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[HitResult.Miss] = 0,
[HitResult.Meh] = 0,
[HitResult.Great] = 0
}, new ScoreProcessorStatistics(), DateTimeOffset.Now);
}, new ScoreProcessorStatistics(), DateTimeOffset.Now, [], 0, []);
}
switch (RNG.Next(0, 3))
@@ -178,7 +178,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
LoadComponent(Overlay = new FreeModSelectOverlay
{
SelectedMods = { BindTarget = FreeMods }
SelectedMods = { BindTarget = FreeMods },
Ruleset = { BindTarget = Ruleset }
});
}
@@ -11,8 +11,11 @@ using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select;
using osu.Game.Utils;
@@ -63,37 +66,45 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestIncrementMultiplier()
{
var ruleset = new OsuRuleset();
var hiddenMod = new Mod[] { new OsuModHidden() };
AddStep("Set ruleset", () => footerButtonMods.Ruleset.Value = ruleset.RulesetInfo);
AddStep(@"Add Hidden", () => changeMods(hiddenMod));
assertModsMultiplier(hiddenMod);
assertModsMultiplier(ruleset, hiddenMod);
var hardRockMod = new Mod[] { new OsuModHardRock() };
AddStep(@"Add HardRock", () => changeMods(hardRockMod));
assertModsMultiplier(hardRockMod);
assertModsMultiplier(ruleset, hardRockMod);
var doubleTimeMod = new Mod[] { new OsuModDoubleTime() };
AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod));
assertModsMultiplier(doubleTimeMod);
assertModsMultiplier(ruleset, doubleTimeMod);
var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() };
AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods));
assertModsMultiplier(multipleIncrementMods);
assertModsMultiplier(ruleset, multipleIncrementMods);
}
[Test]
public void TestDecrementMultiplier()
{
var ruleset = new OsuRuleset();
var easyMod = new Mod[] { new OsuModEasy() };
AddStep("Set ruleset", () => footerButtonMods.Ruleset.Value = ruleset.RulesetInfo);
AddStep(@"Add Easy", () => changeMods(easyMod));
assertModsMultiplier(easyMod);
assertModsMultiplier(ruleset, easyMod);
var noFailMod = new Mod[] { new OsuModNoFail() };
AddStep(@"Add NoFail", () => changeMods(noFailMod));
assertModsMultiplier(noFailMod);
assertModsMultiplier(ruleset, noFailMod);
var multipleDecrementMods = new Mod[] { new OsuModEasy(), new OsuModNoFail() };
AddStep(@"Add Multiple Mods", () => changeMods(multipleDecrementMods));
assertModsMultiplier(multipleDecrementMods);
assertModsMultiplier(ruleset, multipleDecrementMods);
}
[Test]
@@ -105,11 +116,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<FooterButtonMods.UnrankedBadge>().Single().Alpha == 0);
}
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Current.Value = mods;
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Mods.Value = mods;
private void assertModsMultiplier(IEnumerable<Mod> mods)
private void assertModsMultiplier(Ruleset ruleset, IEnumerable<Mod> mods)
{
double multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
double multiplier = scoreMultiplierCalculator.CalculateFor(mods);
string expectedValue = ModUtils.FormatScoreMultiplier(multiplier).ToString();
AddAssert($"Displayed multiplier is {expectedValue}", () => footerButtonMods.ChildrenOfType<OsuSpriteText>().First(t => t.Text.ToString().Contains('x')).Text.ToString(), () => Is.EqualTo(expectedValue));
@@ -25,6 +25,8 @@ using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Screens;
using osu.Game.Screens.Footer;
@@ -122,7 +124,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
double multiplier = new OsuScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor(SelectedMods.Value);
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
@@ -137,7 +139,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("two panels active", () => modSelectOverlay.ChildrenOfType<ModPanel>().Count(panel => panel.Active.Value) == 2);
AddAssert("mod multiplier correct", () =>
{
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
double multiplier = new OsuScoreMultiplierCalculator(new ScoreMultiplierContext()).CalculateFor(SelectedMods.Value);
return Precision.AlmostEquals(multiplier, this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value);
});
assertCustomisationToggleState(disabled: false, active: false);
@@ -1087,6 +1089,7 @@ namespace osu.Game.Tests.Visual.UserInterface
State = { Value = Visibility.Visible },
Beatmap = { Value = Beatmap.Value },
SelectedMods = { BindTarget = SelectedMods },
Ruleset = { BindTarget = Ruleset },
ShowPresets = true,
});
}
@@ -244,10 +244,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new VideoIconPill());
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new StoryboardIconPill());
if (BeatmapSet.FeaturedInSpotlight)
{
@@ -226,10 +226,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new VideoIconPill());
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new StoryboardIconPill());
if (BeatmapSet.FeaturedInSpotlight)
{
@@ -120,6 +120,20 @@ namespace osu.Game.Beatmaps.Formats
switch (type)
{
case LegacyEventType.Background:
{
// the actual filename is handled in `LegacyBeatmapDecoder`.
// this only handles the background offset, because it does not logically belong in `Beatmap` or related classes.
if (split.Length > 4)
{
float x = Parsing.ParseFloat(split[3]);
float y = Parsing.ParseFloat(split[4]);
storyboard.BackgroundOffset = new Vector2(x, y);
}
break;
}
case LegacyEventType.Video:
{
int offset = Parsing.ParseInt(split[1]);
@@ -54,10 +54,9 @@ namespace osu.Game.Beatmaps.Formats
if (target == StoryboardElementSource.Beatmap)
{
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1499
// TODO: handle nonzero background offset (https://github.com/ppy/osu/issues/14238)
writer.WriteLine(string.Format(CultureInfo.InvariantCulture,
@"{0},{1},""{2}"",{3},{4}",
(int)LegacyEventType.Background, 0, storyboard.BeatmapInfo.Metadata.BackgroundFile, 0, 0));
(int)LegacyEventType.Background, 0, storyboard.BeatmapInfo.Metadata.BackgroundFile, storyboard.BackgroundOffset.X, storyboard.BackgroundOffset.Y));
}
// https://github.com/peppy/osu-stable-reference/blob/c34a74fb61c17c5667486a12548485d1f03baa2e/osu!/GameplayElements/HitObjectManager_LoadSave.cs#L1496
@@ -74,6 +74,8 @@ namespace osu.Game.Database
using var storyboardStreamReader = new LineBufferedReader(storyboardStream);
var beatmapStoryboard = new LegacyStoryboardDecoder().Decode(storyboardStreamReader);
beatmapStoryboard.Beatmap = beatmapContent;
beatmapStoryboard.BeatmapInfo = beatmapInfo;
MutateBeatmap(model, playableBeatmap);
@@ -187,10 +187,9 @@ namespace osu.Game.Database
break;
}
double modMultiplier = 1;
foreach (var mod in score.Mods)
modMultiplier *= mod.ScoreMultiplier;
var ruleset = score.Ruleset.CreateInstance();
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(score));
double modMultiplier = scoreMultiplierCalculator.CalculateFor(score.Mods);
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
@@ -352,7 +351,8 @@ namespace osu.Game.Database
long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio);
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
var modMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
double modMultiplier = modMultiplierCalculator.CalculateFor(score.Mods);
long convertedTotalScoreWithoutMods;
+3 -3
View File
@@ -107,7 +107,7 @@ namespace osu.Game.Graphics
public static IconUsage EditorDistanceSnap => get(OsuIconMapping.EditorDistanceSnap);
public static IconUsage EditorFinish => get(OsuIconMapping.EditorFinish);
public static IconUsage EditorGridSnap => get(OsuIconMapping.EditorGridSnap);
public static IconUsage EditorNewCombo => get(OsuIconMapping.EditorNewCombo);
public static IconUsage EditorNewComboSparkles => get(OsuIconMapping.EditorNewComboSparkles);
public static IconUsage EditorSelect => get(OsuIconMapping.EditorSelect);
public static IconUsage EditorSound => get(OsuIconMapping.EditorSound);
public static IconUsage EditorWhistle => get(OsuIconMapping.EditorWhistle);
@@ -459,8 +459,8 @@ namespace osu.Game.Graphics
[Description(@"Editor/grid-snap")]
EditorGridSnap,
[Description(@"Editor/new-combo")]
EditorNewCombo,
[Description(@"Editor/new-combo-sparkles")]
EditorNewComboSparkles,
[Description(@"Editor/select")]
EditorSelect,
@@ -1011,6 +1011,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.AutoStartDuration = Room.Settings.AutoStartDuration;
APIRoom.CurrentPlaylistItem = APIRoom.Playlist.Single(item => item.ID == settings.PlaylistItemId);
APIRoom.AutoSkip = Room.Settings.AutoSkip;
APIRoom.MaxParticipants = Room.Settings.MaxParticipants;
SettingsChanged?.Invoke(settings);
RoomUpdated?.Invoke();
+41 -5
View File
@@ -62,12 +62,33 @@ namespace osu.Game.Online.Spectator
/// The set of mods currently active.
/// </summary>
/// <remarks>
/// Nullable for backwards compatibility with older clients
/// (these structures are also used server-side, and <see langword="null"/> will be used as marker that the data isn't there).
/// can be made non-nullable 20250407
/// This is sent to spectator as mods can change during a play - one relevant circumstance
/// is the automatic activation of Touch Device mod when usage of touch devices is detected.
/// </remarks>
[Key(7)]
public APIMod[]? Mods { get; set; }
public APIMod[] Mods { get; set; } = [];
/// <summary>
/// The current total score without mod multipliers active.
/// </summary>
/// <remarks>
/// Nullable for backwards compatibility with older clients that don't send this
/// (server-side <see langword="null"/> is used to distinguish the lack of this data).
/// can be made non-nullable 20261126
/// </remarks>
[Key(8)]
public long? TotalScoreWithoutMods { get; set; }
/// <summary>
/// The list of time instants in the play at which the player paused the game.
/// </summary>
/// <remarks>
/// Nullable for backwards compatibility with older clients that don't send this
/// (server-side <see langword="null"/> is used to distinguish the lack of this data).
/// can be made non-nullable 20261126
/// </remarks>
[Key(9)]
public int[]? Pauses { get; set; }
/// <summary>
/// Construct header summary information from a point-in-time reference to a score which is actively being played.
@@ -83,13 +104,25 @@ namespace osu.Game.Online.Spectator
// copy for safety
Statistics = new Dictionary<HitResult, int>(score.Statistics);
Mods = score.APIMods.ToArray();
TotalScoreWithoutMods = score.TotalScoreWithoutMods;
Pauses = score.Pauses.ToArray();
ScoreProcessorStatistics = statistics;
}
[JsonConstructor]
[SerializationConstructor]
public FrameHeader(long totalScore, double accuracy, int combo, int maxCombo, Dictionary<HitResult, int> statistics, ScoreProcessorStatistics scoreProcessorStatistics, DateTimeOffset receivedTime)
public FrameHeader(
long totalScore,
double accuracy,
int combo,
int maxCombo,
Dictionary<HitResult, int> statistics,
ScoreProcessorStatistics scoreProcessorStatistics,
DateTimeOffset receivedTime,
APIMod[] mods,
long? totalScoreWithoutMods,
int[]? pauses)
{
TotalScore = totalScore;
Accuracy = accuracy;
@@ -98,6 +131,9 @@ namespace osu.Game.Online.Spectator
Statistics = statistics;
ScoreProcessorStatistics = scoreProcessorStatistics;
ReceivedTime = receivedTime;
Mods = mods;
TotalScoreWithoutMods = totalScoreWithoutMods;
Pauses = pauses;
}
}
}
+8 -1
View File
@@ -1301,10 +1301,17 @@ namespace osu.Game
applyConfigMigrations();
string lastVersion = LocalConfig.Get<string>(OsuSetting.Version);
string version = Version;
// only show a notification if we've previously saved a version to the config file (ie. not the first run).
if (IsDeployedBuild && !string.IsNullOrEmpty(lastVersion) && version != lastVersion)
Notifications.Post(new UpdateCompleteNotification(version));
// finally, update the version stored to the configuration.
// this MUST happen after `applyConfigMigrations()` call, as it relies on comparing the previous version.
// debug / local compilations will reset to a non-release string.
LocalConfig.SetValue(OsuSetting.Version, Version);
LocalConfig.SetValue(OsuSetting.Version, version);
}
/// <summary>
@@ -10,7 +10,9 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Overlays.Mods
@@ -28,6 +30,7 @@ namespace osu.Game.Overlays.Mods
public readonly IBindable<WorkingBeatmap?> Beatmap = new Bindable<WorkingBeatmap?>();
public readonly IBindable<IReadOnlyList<Mod>> ActiveMods = new Bindable<IReadOnlyList<Mod>>();
public readonly IBindable<RulesetInfo?> Ruleset = new Bindable<RulesetInfo?>();
/// <summary>
/// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown.
@@ -101,6 +104,7 @@ namespace osu.Game.Overlays.Mods
beatmapAttributesDisplay.BeatmapInfo.Value = b.NewValue?.BeatmapInfo;
}, true);
Ruleset.BindValueChanged(_ => updateInformation());
ActiveMods.BindValueChanged(m =>
{
updateInformation();
@@ -120,10 +124,8 @@ namespace osu.Game.Overlays.Mods
{
if (rankingInformationDisplay != null)
{
double multiplier = 1.0;
foreach (var mod in ActiveMods.Value)
multiplier *= mod.ScoreMultiplier;
var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
double multiplier = scoreMultiplierCalculator?.CalculateFor(ActiveMods.Value) ?? 1;
rankingInformationDisplay.ModMultiplier.Value = multiplier;
rankingInformationDisplay.Ranked.Value = ActiveMods.Value.All(m => m.Ranked);
@@ -26,6 +26,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Utils;
@@ -121,6 +122,7 @@ namespace osu.Game.Overlays.Mods
private Sample? columnAppearSample;
public readonly Bindable<WorkingBeatmap?> Beatmap = new Bindable<WorkingBeatmap?>();
public readonly Bindable<RulesetInfo?> Ruleset = new Bindable<RulesetInfo?>();
[Resolved]
private ScreenFooter? footer { get; set; }
@@ -283,6 +285,7 @@ namespace osu.Game.Overlays.Mods
{
Beatmap = { BindTarget = Beatmap },
ActiveMods = { BindTarget = ActiveMods },
Ruleset = { BindTarget = Ruleset },
};
private static readonly LocalisableString input_search_placeholder = Resources.Localisation.Web.CommonStrings.InputSearch;
+1 -1
View File
@@ -213,7 +213,7 @@ namespace osu.Game.Rulesets
/// <summary>
/// Creates a <see cref="ScoreMultiplierCalculator"/> relevant to this ruleset.
/// </summary>
public virtual ScoreMultiplierCalculator CreateScoreMultiplierCalculator() => new ScoreMultiplierCalculator();
public virtual ScoreMultiplierCalculator CreateScoreMultiplierCalculator(ScoreMultiplierContext context) => new ScoreMultiplierCalculator(context);
/// <summary>
/// Create a transformer which adds lookups specific to a ruleset to skin sources.
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Scoring
{
@@ -13,47 +14,42 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public class ScoreMultiplierCalculator
{
private static readonly List<(Type[] mods, Func<Mod[], double> multiplier)> combination_multipliers = [];
private static readonly Dictionary<Type, Func<Mod, ScoreMultiplierCalculator, double>> single_multipliers_with_context = [];
private static readonly Dictionary<Type, Func<Mod, double>> single_multipliers = [];
protected ScoreMultiplierContext Context { get; }
private readonly List<(Type[] mods, Func<Mod[], double> multiplier)> combinationMultipliers = [];
private readonly Dictionary<Type, Func<Mod, double>> singleMultipliers = [];
public ScoreMultiplierCalculator(ScoreMultiplierContext context)
{
Context = context;
}
/// <summary>
/// Defines a flat, setting-independent score multiplier for the given <typeparamref name="TMod"/>.
/// </summary>
public static void Single<TMod>(double hasMultiplier)
protected void Single<TMod>(double hasMultiplier)
where TMod : Mod
{
single_multipliers[typeof(TMod)] = _ => hasMultiplier;
singleMultipliers[typeof(TMod)] = _ => hasMultiplier;
}
/// <summary>
/// Defines a setting-dependent score multiplier for the given <typeparamref name="TMod"/>.
/// </summary>
public static void Single<TMod>(Func<TMod, double> hasMultiplier)
protected void Single<TMod>(Func<TMod, double> hasMultiplier)
where TMod : Mod
{
single_multipliers[typeof(TMod)] = mod => hasMultiplier.Invoke((TMod)mod);
}
/// <summary>
/// Defines a setting-dependent score multiplier for the given <typeparamref name="TMod"/>.
/// The multiplier calculation is given additional context to calculate the multiplier via the <typeparamref name="TContext"/> type instance.
/// </summary>
public static void Single<TMod, TContext>(Func<TMod, TContext, double> hasMultiplier)
where TMod : Mod
where TContext : ScoreMultiplierCalculator
{
single_multipliers_with_context[typeof(TMod)] = (mod, context) => hasMultiplier.Invoke((TMod)mod, (TContext)context);
singleMultipliers[typeof(TMod)] = mod => hasMultiplier.Invoke((TMod)mod);
}
/// <summary>
/// Defines a score multiplier specific to when both <typeparamref name="T1"/> and <typeparamref name="T2"/> mods are present.
/// </summary>
public static void Combination<T1, T2>(Func<T1, T2, double> hasMultiplier)
protected void Combination<T1, T2>(Func<T1, T2, double> hasMultiplier)
where T1 : Mod
where T2 : Mod
{
combination_multipliers.Add(([typeof(T1), typeof(T2)], mods => hasMultiplier((T1)mods[0], (T2)mods[1])));
combinationMultipliers.Add(([typeof(T1), typeof(T2)], mods => hasMultiplier((T1)mods[0], (T2)mods[1])));
}
/// <summary>
@@ -72,7 +68,7 @@ namespace osu.Game.Rulesets.Scoring
if (allModsByType.Count > 1)
{
foreach (var (combination, multiplier) in combination_multipliers)
foreach (var (combination, multiplier) in combinationMultipliers)
{
if (remainingModTypes.IsSupersetOf(combination))
{
@@ -85,13 +81,48 @@ namespace osu.Game.Rulesets.Scoring
foreach (var modType in remainingModTypes)
{
if (single_multipliers.TryGetValue(modType, out var multiplier))
if (singleMultipliers.TryGetValue(modType, out var multiplier))
result *= multiplier(allModsByType[modType]);
else if (single_multipliers_with_context.TryGetValue(modType, out var multiplierWithContext))
result *= multiplierWithContext(allModsByType[modType], this);
}
return result;
}
}
/// <summary>
/// Contextual information to pass to a <see cref="ScoreMultiplierContext"/>
/// in order for it to calculate the correct multiplier.
/// </summary>
public class ScoreMultiplierContext
{
/// <summary>
/// The score that the multipliers are calculated for.
/// Mostly relevant and present in backwards compatibility scenarios.
/// In usages where the current valid score multipliers are required, pass <see langword="null"/> or use a constructor that does not require this.
/// </summary>
public ScoreInfo? Score { get; }
/// <summary>
/// Constructs a new instance.
/// Use this in situations wherein the current valid score multipliers are needed.
/// </summary>
public ScoreMultiplierContext()
: this(null)
{
}
/// <summary>
/// Constructs a new instance.
/// Use this in backwards compatibility scenarios when dealing with a specific <paramref name="score"/>.
/// </summary>
/// <param name="score">
/// The score that the multipliers are calculated for.
/// Mostly relevant and present in backwards compatibility scenarios.
/// In usages where the current valid score multipliers are required, pass <see langword="null"/> or use a constructor that does not require this.
/// </param>
public ScoreMultiplierContext(ScoreInfo? score)
{
Score = score;
}
}
}
+2 -4
View File
@@ -206,10 +206,8 @@ namespace osu.Game.Rulesets.Scoring
Mods.ValueChanged += mods =>
{
scoreMultiplier = 1;
foreach (var m in mods.NewValue)
scoreMultiplier *= m.ScoreMultiplier;
var calculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
scoreMultiplier = calculator.CalculateFor(mods.NewValue);
updateScore();
updateRank();
@@ -258,10 +258,9 @@ namespace osu.Game.Scoring.Legacy
public static void PopulateTotalScoreWithoutMods(ScoreInfo score)
{
double modMultiplier = 1;
foreach (var mod in score.Mods)
modMultiplier *= mod.ScoreMultiplier;
var ruleset = score.Ruleset.CreateInstance();
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext(score));
double modMultiplier = scoreMultiplierCalculator.CalculateFor(score.Mods);
score.TotalScoreWithoutMods = (long)Math.Round(score.TotalScore / modMultiplier);
}
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions;
using osu.Game.IO.Legacy;
using osu.Game.IO.Serialization;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Replays;
@@ -20,6 +21,19 @@ using SharpCompress.Compressors.LZMA;
namespace osu.Game.Scoring.Legacy
{
/// <summary>
/// Encodes replays.
/// </summary>
/// <remarks>
/// <b>When making <i>ANY</i> changes to the replay format to add new data, consider if:</b>
/// <list type="bullet">
/// <item><see cref="LATEST_VERSION"/> should be bumped accordingly,</item>
/// <item>
/// changes need to be made to <see cref="SpectatorClient"/> so that spectator server receives the new data being stored,
/// as <b><i>spectator server</i> is responsible for the content of server-stored replays, <i>NOT</i> the client</b>.
/// </item>
/// </list>
/// </remarks>
public class LegacyScoreEncoder
{
/// <summary>
@@ -29,6 +29,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public partial class NewComboTernaryButton : CompositeDrawable, IHasCurrentValue<TernaryState>
{
public Func<Drawable>? CreateIcon { get; init; }
public Bindable<TernaryState> Current
{
get => current.Current;
@@ -61,7 +63,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
Current = Current,
Description = "New combo",
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewCombo },
CreateIcon = CreateIcon,
},
},
pickerButton = new ColourPickerButton
@@ -181,13 +181,50 @@ namespace osu.Game.Screens.Edit.Compose.Components
public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; }
/// <summary>
/// Create the new combo ternary button. Mainly used to customize the displayed icon
/// depending on the ruleset. Can be overriden to return null if a ruleset does not
/// provide combo-supporting HitObjects.
/// </summary>
/// <returns></returns>
[CanBeNull]
protected virtual Drawable CreateNewComboButton() => new NewComboTernaryButton
{
Current = NewCombo,
CreateIcon = () => new Container
{
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
// This is currently using the osu! hitcircle icon as a default in order
// not to break any custom rulesets that depend on there being a defined
// new combo button.
// Could consider removing it and let rulesets specify their own buttons/icons.
Icon = OsuIcon.EditorHitCircle,
Size = new Vector2(15),
},
new SpriteIcon
{
Icon = OsuIcon.EditorNewComboSparkles,
Size = new Vector2(20),
}
},
},
};
/// <summary>
/// Create all ternary states required to be displayed to the user.
/// </summary>
protected virtual IEnumerable<Drawable> CreateTernaryButtons()
{
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
yield return new NewComboTernaryButton { Current = NewCombo };
var newComboButton = CreateNewComboButton();
if (newComboButton != null)
yield return newComboButton;
foreach (var kvp in SelectionHandler.SelectionSampleStates)
{
@@ -315,6 +315,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
Beatmap = { BindTarget = Beatmap },
SelectedMods = { BindTarget = userMods },
Ruleset = { BindTarget = Ruleset },
IsValidMod = _ => false
});
@@ -39,6 +39,7 @@ namespace osu.Game.Screens.OnlinePlay
{
Beatmap = { BindTarget = Beatmap },
ActiveMods = { BindTarget = ActiveMods },
Ruleset = { BindTarget = Ruleset },
};
public partial class FreeModSelectFooterContent : ModSelectFooterContent
@@ -307,10 +307,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
};
if (beatmapSet.HasVideo)
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new VideoIconPill());
if (beatmapSet.HasStoryboard)
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
leftIconArea.Add(new StoryboardIconPill());
if (beatmapSet.HasExplicitContent)
{
@@ -77,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private DrawableSample resultsAppearSample = null!;
private DrawableSample dmgFlySample = null!;
private DrawableSample dmgHitSample = null!;
private DrawableSample damageMultiplierSample = null!;
private DrawableSample damageMultiplierUpSample = null!;
private DrawableSample damageMultiplierDownSample = null!;
private DrawableSample hpDownSample = null!;
private DrawableSample playerAppearSample = null!;
private DrawableSample pseudoScoreCounterSample = null!;
@@ -337,6 +340,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
resultsAppearSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/results-appear")),
dmgFlySample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-fly")),
dmgHitSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-hit")),
damageMultiplierSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-multiplier")),
damageMultiplierUpSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-multiplier-up")),
damageMultiplierDownSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-multiplier-down")),
hpDownSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/hp-down")),
playerAppearSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/players-appear")),
pseudoScoreCounterSample = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/pseudo-score-counter")),
@@ -515,14 +521,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
));
}
int pitchChangeAmount = 0;
foreach (var breakdown in damageBreakdowns)
{
using (BeginDelayedSequence(delay))
{
int pitch = pitchChangeAmount;
Schedule(() =>
{
damageBreakdownValueText.Text = breakdown.displayValue;
damageBreakdownSourceText.Text = breakdown.source;
SampleChannel damageBreakdownChannel = damageMultiplierSample.GetChannel();
damageBreakdownChannel.Frequency.Value = 1f + (pitch * .1f);
damageBreakdownChannel.Play();
});
damageBreakdownContainer.MoveToX(120)
@@ -540,7 +554,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
.ScaleTo(new Vector2(1.25f), 200, Easing.OutQuint)
.Then()
.ScaleTo(Vector2.One, 200);
SampleChannel scoreChangeChannel = breakdown.rawDamage > 0 ? damageMultiplierUpSample.GetChannel() : damageMultiplierDownSample.GetChannel();
scoreChangeChannel.Frequency.Value = 1f + (pitch * .1f);
scoreChangeChannel.Play();
});
if (breakdown.rawDamage > 0)
pitchChangeAmount++;
else if (breakdown.rawDamage < 0)
pitchChangeAmount--;
}
}
@@ -291,7 +291,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override ModSelectOverlay CreateModSelectOverlay() => modSelect = new UserModSelectOverlay(OverlayColourScheme.Plum)
{
IsValidMod = isValidRequiredMod
IsValidMod = isValidRequiredMod,
};
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons()
@@ -421,7 +421,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
LoadComponent(userModsSelectOverlay = new MultiplayerUserModSelectOverlay
{
Beatmap = { BindTarget = Beatmap }
Beatmap = { BindTarget = Beatmap },
Ruleset = { BindTarget = Ruleset },
});
}
@@ -63,7 +63,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
for (byte i = 0; i < slotUserIds.Length; ++i)
{
var participant = slotUserIds[i] == null ? Slot.Empty(i) : Slot.FromUser(client.Room.Users.Single(u => u.UserID == slotUserIds[i]));
var user = slotUserIds[i] != null ? client.Room.Users.SingleOrDefault(u => u.UserID == slotUserIds[i]) : null;
var participant = user == null ? Slot.Empty(i) : Slot.FromUser(client.Room.Users.Single(u => u.UserID == slotUserIds[i]));
if (i >= slots.Count)
slots.Add(participant);
@@ -445,6 +445,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
SelectedItem = { BindTarget = SelectedItem },
SelectedMods = { BindTarget = UserMods },
Beatmap = { BindTarget = Beatmap },
Ruleset = { BindTarget = Ruleset },
IsValidMod = _ => false
});
}
@@ -228,7 +228,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override ModSelectOverlay CreateModSelectOverlay() => modSelect = new UserModSelectOverlay(OverlayColourScheme.Plum)
{
IsValidMod = isValidRequiredMod
IsValidMod = isValidRequiredMod,
};
private PlaylistItem createItem() => new PlaylistItem(Beatmap.Value.BeatmapInfo)
@@ -28,6 +28,7 @@ using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Utils;
@@ -120,10 +121,9 @@ namespace osu.Game.Screens.Select
var judgementsStatistics = value.GetStatisticsForDisplay().Select(s =>
new StatisticRow(s.DisplayName.ToUpper(), s.Count.ToLocalisableString("N0"), colours.ForHitResult(s.Result)));
double multiplier = 1.0;
foreach (var mod in value.Mods)
multiplier *= mod.ScoreMultiplier;
var ruleset = value.Ruleset.CreateInstance();
var scoreMultiplierCalculator = ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
double multiplier = scoreMultiplierCalculator.CalculateFor(value.Mods);
var generalStatistics = new[]
{
+24 -13
View File
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Configuration;
@@ -22,7 +21,9 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Play.HUD;
using osu.Game.Utils;
@@ -32,7 +33,7 @@ using osuTK.Input;
namespace osu.Game.Screens.Select
{
public partial class FooterButtonMods : ScreenFooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
public partial class FooterButtonMods : ScreenFooterButton
{
public Action? RequestDeselectAllMods { get; init; }
@@ -40,12 +41,20 @@ namespace osu.Game.Screens.Select
private const float mod_display_portion = 0.65f;
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private readonly BindableWithCurrent<IReadOnlyList<Mod>> mods = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public Bindable<IReadOnlyList<Mod>> Current
public Bindable<IReadOnlyList<Mod>> Mods
{
get => current.Current;
set => current.Current = value;
get => mods.Current;
set => mods.Current = value;
}
private readonly BindableWithCurrent<RulesetInfo?> ruleset = new BindableWithCurrent<RulesetInfo?>();
public Bindable<RulesetInfo?> Ruleset
{
get => ruleset.Current;
set => ruleset.Current = value;
}
private Container modDisplayBar = null!;
@@ -145,10 +154,10 @@ namespace osu.Game.Screens.Select
Origin = Anchor.Centre,
Shear = -OsuGame.SHEAR,
Scale = new Vector2(0.5f),
Current = { BindTarget = Current },
Current = { BindTarget = Mods },
ExpansionMode = ExpansionMode.AlwaysContracted,
},
overflowModCountDisplay = new ModCountText { Mods = { BindTarget = Current }, },
overflowModCountDisplay = new ModCountText { Mods = { BindTarget = Mods }, },
}
},
}
@@ -165,7 +174,8 @@ namespace osu.Game.Screens.Select
currentLanguage = game.CurrentLanguage.GetBoundCopy();
currentLanguage.BindValueChanged(_ => ScheduleAfterChildren(updateDisplay));
Current.BindValueChanged(m =>
Ruleset.BindValueChanged(_ => updateDisplay());
Mods.BindValueChanged(m =>
{
modSettingChangeTracker?.Dispose();
@@ -198,7 +208,7 @@ namespace osu.Game.Screens.Select
private void updateDisplay()
{
if (Current.Value.Count == 0)
if (Mods.Value.Count == 0)
{
modDisplayBar.MoveToY(20, duration, easing);
modDisplayBar.FadeOut(duration, easing);
@@ -213,7 +223,7 @@ namespace osu.Game.Screens.Select
}
else
{
if (Current.Value.Any(m => !m.Ranked))
if (Mods.Value.Any(m => !m.Ranked))
{
unrankedBadge.MoveToX(0, duration, easing);
unrankedBadge.FadeIn(duration, easing);
@@ -234,7 +244,8 @@ namespace osu.Game.Screens.Select
modDisplay.FadeIn(duration, easing);
}
double multiplier = Current.Value?.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier) ?? 1;
var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
double multiplier = scoreMultiplierCalculator?.CalculateFor(Mods.Value) ?? 1;
multiplierText.Text = ModUtils.FormatScoreMultiplier(multiplier);
if (multiplier > 1)
@@ -249,7 +260,7 @@ namespace osu.Game.Screens.Select
{
base.Update();
if (Current.Value.Count == 0)
if (Mods.Value.Count == 0)
return;
if (modDisplay.DrawWidth * modDisplay.Scale.X > modContainer.DrawWidth)
+4 -1
View File
@@ -351,7 +351,8 @@ namespace osu.Game.Screens.Select
new FooterButtonMods(modSelectOverlay)
{
Hotkey = GlobalAction.ToggleModSelection,
Current = Mods,
Mods = Mods,
Ruleset = Ruleset,
RequestDeselectAllMods = () =>
{
if (modSelectOverlay.State.Value == Visibility.Visible)
@@ -708,6 +709,7 @@ namespace osu.Game.Screens.Select
private void onArrivingAtScreen()
{
modSelectOverlay.Beatmap.BindTo(Beatmap);
modSelectOverlay.Ruleset.BindTo(Ruleset);
// required due to https://github.com/ppy/osu-framework/issues/3218
modSelectOverlay.SelectedMods.Disabled = false;
modSelectOverlay.SelectedMods.BindTo(Mods);
@@ -755,6 +757,7 @@ namespace osu.Game.Screens.Select
Beatmap.ValueChanged -= updateVariousState;
modSelectOverlay.SelectedMods.UnbindFrom(Mods);
modSelectOverlay.Ruleset.UnbindFrom(Ruleset);
modSelectOverlay.Beatmap.UnbindFrom(Beatmap);
updateWedgeVisibility();
+8 -1
View File
@@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards.Drawables;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Storyboards
{
@@ -17,7 +18,7 @@ namespace osu.Game.Storyboards
private readonly Dictionary<string, StoryboardLayer> layers = new Dictionary<string, StoryboardLayer>();
public IEnumerable<StoryboardLayer> Layers => layers.Values;
public BeatmapInfo BeatmapInfo = new BeatmapInfo();
public BeatmapInfo BeatmapInfo { get; set; } = new BeatmapInfo();
public IBeatmap Beatmap { get; set; } = new Beatmap();
/// <summary>
@@ -98,6 +99,12 @@ namespace osu.Game.Storyboards
}
}
/// <summary>
/// Offset to be applied to the beatmap background.
/// TODO: Unused yet. See https://github.com/ppy/osu/issues/14238.
/// </summary>
public Vector2 BackgroundOffset { get; set; } = Vector2.Zero;
public virtual DrawableStoryboard CreateDrawable(IReadOnlyList<Mod>? mods = null) =>
new DrawableStoryboard(this, mods);
@@ -3,9 +3,10 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Utils;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Tests.Rulesets
{
@@ -21,33 +22,20 @@ namespace osu.Game.Tests.Rulesets
[Test]
public void TestDefaultMultiplierIsOne()
{
var calculator = Ruleset.CreateScoreMultiplierCalculator();
Assert.That(calculator.CalculateFor([]), Is.EqualTo(1));
}
=> TestModCombination([], 1);
[Test]
public void TestMultipliersMatchForIndividualMods()
protected void TestModCombination(IEnumerable<Mod> mods, double expectedMultiplier)
{
var mods = Ruleset.CreateAllMods();
var calculator = Ruleset.CreateScoreMultiplierCalculator();
Assert.Multiple(() =>
{
double multiplierViaOldAPI = 1;
foreach (var mod in mods)
Assert.That(calculator.CalculateFor(mod.Yield()), Is.EqualTo(mod.ScoreMultiplier), message: $"Score multiplier not matching for mod {mod.Name}");
multiplierViaOldAPI *= mod.ScoreMultiplier;
Assert.That(multiplierViaOldAPI, Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
var calculator = Ruleset.CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
Assert.That(calculator.CalculateFor(mods), Is.EqualTo(expectedMultiplier).Within(Precision.DOUBLE_EPSILON));
});
}
protected void TestModCombination(IEnumerable<Mod> mods)
{
var calculator = Ruleset.CreateScoreMultiplierCalculator();
double expected = 1;
foreach (var mod in mods)
expected *= mod.ScoreMultiplier;
Assert.That(calculator.CalculateFor(mods), Is.EqualTo(expected));
}
}
}
+25 -32
View File
@@ -56,15 +56,8 @@ namespace osu.Game.Updater
{
base.LoadComplete();
string version = game.Version;
string lastVersion = config.Get<string>(OsuSetting.Version);
if (game.IsDeployedBuild)
{
// only show a notification if we've previously saved a version to the config file (ie. not the first run).
if (!string.IsNullOrEmpty(lastVersion) && version != lastVersion)
Notifications.Post(new UpdateCompleteNotification(version));
// make sure the release stream setting matches the build which was just run.
if (FixedReleaseStream != null)
config.SetValue(OsuSetting.ReleaseStream, FixedReleaseStream.Value);
@@ -137,31 +130,6 @@ namespace osu.Game.Updater
updateCancellationSource.Dispose();
}
private partial class UpdateCompleteNotification : SimpleNotification
{
private readonly string version;
public UpdateCompleteNotification(string version)
{
this.version = version;
Text = NotificationsStrings.GameVersionAfterUpdate(version);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, ChangelogOverlay changelog, INotificationOverlay notificationOverlay)
{
Icon = FontAwesome.Solid.CheckSquare;
IconContent.Colour = colours.BlueDark;
Activated = delegate
{
notificationOverlay.Hide();
changelog.ShowBuild(version);
return true;
};
}
}
public partial class UpdateDownloadProgressNotification : ProgressNotification
{
private readonly CancellationToken cancellationToken;
@@ -259,4 +227,29 @@ namespace osu.Game.Updater
}
}
}
public partial class UpdateCompleteNotification : SimpleNotification
{
private readonly string version;
public UpdateCompleteNotification(string version)
{
this.version = version;
Text = NotificationsStrings.GameVersionAfterUpdate(version);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, ChangelogOverlay changelog, INotificationOverlay notificationOverlay)
{
Icon = FontAwesome.Solid.CheckSquare;
IconContent.Colour = colours.BlueDark;
Activated = delegate
{
notificationOverlay.Hide();
changelog.ShowBuild(version);
return true;
};
}
}
}
+1 -1
View File
@@ -40,7 +40,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2026.521.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.521.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.523.0" />
<PackageReference Include="Sentry" Version="6.2.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.48.0" />