1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-27 04:19:54 +08:00

Compare commits

...

9 Commits

  • Fix client not sending data relevant to replay to spectator server (#37919)
    - Related to https://github.com/ppy/osu/issues/37818, but of no material
    help to it at this point (too late for that)
    
    As noted in
    https://github.com/ppy/osu/pull/37845#discussion_r3297203361.
    
    Upon comparison of replays recorded by the client and by the server the
    affected fields are: total score without mods, and the list of user
    pauses. Additionally, the date of setting the score may differ -
    server-side it seems to be written with UTC+0 while client-side it's
    written using the local timezone offset. Not really interested in fixing
    that last issue at this time.
    
    Also included is an intentionally loud disclaimer in
    `LegacyScoreEncoder` to tread with caution when treating the class. Not
    sure it'll help, and it's a bit late for it as pretty much every single
    versioning primitive has been ravaged to the brink of unusability, but
    maybe it'll help someone in the future.
    
    This also cleans up an unnecessary nullable on `FrameHeader.Mods` (added
    in https://github.com/ppy/osu/pull/30137). This change can be only done
    if users on releases earlier than 2024.1023.0 can no longer connect to
    spectator server. I leave it to reviewers to determine this as I have no
    visibility over current spectator server configuration. Inspecting the
    `osu_builds` table may help confirm this. If it provokes unease, I can
    back this change out.
  • Replace usages of Mod.ScoreMultiplier with new score multiplier API (#37845)
    - Part of https://github.com/ppy/osu/issues/37818
    
    During review, I would like to direct particular attention to the
    following changes:
    
    ## [Migrate song select to new score multiplier
    API](https://github.com/ppy/osu/commit/945fd78539da3ae57d1550a5bbfb0f859d153cc4)
    
    This was a confusing change to write because of the way song selects
    hook their mod overlays up to global bindables. In particular different
    things happen in different circumstances.
    
    - When going through `SongSelect.CreateModOverlay()`, which is called by
    the base `SongSelect`, the mod overlay is automatically bound to global
    bindables via `SongSelect.on{ArrivingAt,Leaving}Screen()`.
    - For multiplayer user mod select overlays, which are bolted on by
    subclasses of `SongSelect`, manual hook-up is required.
    - As for free mod select overlays, they don't show mod multipliers at
    all, and don't have easy access to the ruleset, and thus the hookup is
    skipped entirely as redundant.
    
    ## [Fix score multiplier registrations being shared between
    implementations via superclass static
    fields](https://github.com/ppy/osu/commit/ba0a7ad421e0c84c2d8162b6bbdd3a0683f5a6a6)
    
    Revealed by `ScoreMultiplierCalculatorTest` starting to fail due to
    interference from `OsuScoreMultiplierCalculator`.
    
    It's not ideal from a performance standpoint but it's the simplest
    choice for now. Tricks could be pulled to salvage the static. One is
    
    ```csharp
    public class ScoreMultiplierCalculator<T>
    	where T : ScoreMultiplierCalculator<T>
    {
    }
    ```
    
    This works because of generics internals; static instance members are
    not shared between different specialisations of a generic class. It is
    also very unintuitive, so I would rather not. (It trips a ReSharper
    inspection too, which would have to be silenced.)
    
    From a performance standpoint this is not ideal, but a significant chunk
    of migrated usages already precede the construction of the calculator
    via the known-expensive `RulesetInfo.CreateInstance()`, and the paths
    that actually construct the calculator do not appear to be that hot. If
    need be, this can be handled by actually caching ruleset instances and
    their derivative subcomponents.
    
    ## [Introduce passing of context to score multiplier
    calculator](https://github.com/ppy/osu/pull/37845/changes/9e9242b3221dddacd226f4b3b9c5632d7350e998)
    
    This is required for two reasons:
    
    - The upcoming mod rebalance will require out-of-band supplementary
    information that is not available for reading from the mod instances
    themselves for calculating the multiplier.
    - This context, namely passing of `ScoreInfo`, will be used for
    implementing backwards compatibility with old scores and their score
    multipliers. This is required because it has turned out under inspection
    that all server-side lazer replays recorded until now are missing
    `TotalScoreWithoutMods` due to an omission of not sending it across the
    wire to spectator server.
    
    Because the score import flow uses replays, filtered through
    `LegacyScoreDecoder`, to populate total score in the realm database, it
    is basically impossible to ignore scores that are missing
    `TotalScoreWithoutMods`, because that will result in bug reports that
    the scores do not have the new score multipliers applied.
    
    Thus, passing of `ScoreInfo` will facilitate implementation of
    versioning score multipliers, which should result in less breakage than
    not doing so.
    
    An example of this is added in 341b2d6e55,
    which should handle the case of mania mod multipliers having been
    changed without any attempt to facilitate for it in
    https://github.com/ppy/osu/pull/30506.
    
    ---------
    
    Co-authored-by: Dean Herbert <pe@ppy.sh>
  • Replace new combo button icons with ruleset-specifc ones (#37848)
    - Depends on https://github.com/ppy/osu-resources/pull/425.
    - Closes https://github.com/ppy/osu/issues/37874
    
    This makes the new combo button use the new icons added in
    https://github.com/ppy/osu/pull/37804. Instead of having four separate
    icons per ruleset, the "sparkle" texture is overlaid on top of the
    appropriate icon.
    
    I'm not sure if I've overdone it with how every ruleset copypastes the
    same code for the icon (in `<ruleset>BlueprintContainer`), so that can
    be scaled down if necessary.
    
    | osu | taiko | catch | mania |
    |--------|--------|--------|--------|
    | <img width="200" height="67" alt="image"
    src="https://github.com/user-attachments/assets/88a31611-f200-4da8-8490-39e6803a452c"
    /> | <img width="194" height="69" alt="image"
    src="https://github.com/user-attachments/assets/fbe5c7c0-2a53-4f3f-9c80-67c8769dfb52"
    /> | <img width="194" height="69" alt="image"
    src="https://github.com/user-attachments/assets/dbfbd183-0469-4b57-9059-40351604aa64"
    /> | <img width="190" height="68" alt="image"
    src="https://github.com/user-attachments/assets/708fc2e0-34fb-4983-b696-8c23431f8af4"
    /> |
    
    Co-authored-by: Dean Herbert <pe@ppy.sh>
  • Follow-up fixes for client-side slots implementation (#37868)
    Fell out of full-stack testing with
    https://github.com/ppy/osu-server-spectator/pull/513.
    
    - **Fix missing property copy in multiplayer client**
    Would cause the participant count limit to not update on the multiplayer
    match screen.
      
    
    - **Fix hard crash when user is kicked from a room with slots active**
    The kicked user is unsubscribed from receiving room state updates before
    their slot is vacated, which then would lead this code to attempt to
    look the local, kicked user via the unvacated slot and thus fail because
    `client.Room.Users` does *not* contain the user anymore.
      
    This is a bit of a dicey change but I think it's less dicey than to try
    to wiggle ordering server-side.
  • Fix legacy beatmap export dropping background specification (#37892)
    - Closes https://github.com/ppy/osu/issues/37884
    - Closes https://github.com/ppy/osu/pull/37890
    
    Due to lack of population of `Storyboard.Beatmap` and
    `Storyboard.BeatmapInfo` post-decoding, `LegacyBeatmapExporter` would
    completely drop background specifications on exported beatmap packages.
    This affects both direct legacy export to file (`.osz`) as well as
    beatmap submission.
    
    I will not pretend that the API here is optimal but I do not see very
    easy opportunities to curtail misuse. Storyboards can be treated as
    either parts of a beatmap or standalone entities, and if a requirement
    is added to forcibly provide a beatmap and its info when encoding out a
    storyboard, I also foresee a requirement to bypass this later when
    design mode is implemented, which would be a return to square one.
    
    There is likely room for cleanup around `Storyboard` to maybe make this
    nicer (remove passing of both `Beatmap` and `BeatmapInfo` and just pass
    `Beatmap` instead, maybe shuffle some properties from `Beatmap` to
    `Storyboard` to remove the requirement of having to bolt the beatmap on
    to begin with). I leave voicing opinions on that, and how soon that
    should be done, to reviewers. My primary intent at this time is to
    hotfix a major issue in a released build.
    
    The external editing feature is not involved in this bug and any
    attempts to claim so are misdirections.
  • Fix "Click to see what's new!" notification no longer appearing (#37875)
    - Regressed with https://github.com/ppy/osu/pull/37839.
    - Closes https://github.com/ppy/osu/issues/37870
    
    I feel like `UpdateManager` should remain a background component, so
    moving this notification into a new stable execution path is best to me.
54 changed files with 1130 additions and 272 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";
@@ -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,
});
}
@@ -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
@@ -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();
+1 -1
View File
@@ -18,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>
@@ -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" />