diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs index f65b6ef381..f931795ff2 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs @@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring.Legacy; @@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; } - int difficultyPeppyStars = (int)Math.Round( - (baseBeatmap.Difficulty.DrainRate - + baseBeatmap.Difficulty.OverallDifficulty - + baseBeatmap.Difficulty.CircleSize - + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); - - scoreMultiplier = difficultyPeppyStars; + scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength); LegacyScoreAttributes attributes = new LegacyScoreAttributes(); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs index a76054b42c..b808deab5c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; @@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; } - int difficultyPeppyStars = (int)Math.Round( - (baseBeatmap.Difficulty.DrainRate - + baseBeatmap.Difficulty.OverallDifficulty - + baseBeatmap.Difficulty.CircleSize - + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); - - scoreMultiplier = difficultyPeppyStars; + scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength); LegacyScoreAttributes attributes = new LegacyScoreAttributes(); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs index b20aa4f2b6..66ff0fc3d9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring.Legacy; @@ -65,11 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000; } - difficultyPeppyStars = (int)Math.Round( - (baseBeatmap.Difficulty.DrainRate - + baseBeatmap.Difficulty.OverallDifficulty - + baseBeatmap.Difficulty.CircleSize - + Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5); + difficultyPeppyStars = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength); LegacyScoreAttributes attributes = new LegacyScoreAttributes(); diff --git a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs index 53cf835248..2a5a11161b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs +++ b/osu.Game/Rulesets/Objects/Legacy/LegacyRulesetExtensions.cs @@ -57,5 +57,40 @@ namespace osu.Game.Rulesets.Objects.Legacy return (float)(1.0f - 0.7f * IBeatmapDifficultyInfo.DifficultyRange(circleSize)) / 2 * (applyFudge ? broken_gamefield_rounding_allowance : 1); } + + public static int CalculateDifficultyPeppyStars(BeatmapDifficulty difficulty, int objectCount, int drainLength) + { + /* + * WARNING: DO NOT TOUCH IF YOU DO NOT KNOW WHAT YOU ARE DOING + * + * It so happens that in stable, due to .NET Framework internals, float math would be performed + * using x87 registers and opcodes. + * .NET (Core) however uses SSE instructions on 32- and 64-bit words. + * x87 registers are _80 bits_ wide. Which is notably wider than _both_ float and double. + * Therefore, on a significant number of beatmaps, the rounding would not produce correct values. + * + * Thus, to crudely - but, seemingly *mostly* accurately, after checking across all ranked maps - emulate this, + * use `decimal`, which is slow, but has bigger precision than `double`. + * At the time of writing, there is _one_ ranked exception to this - namely https://osu.ppy.sh/beatmapsets/1156087#osu/2625853 - + * but it is considered an "acceptable casualty", since in that case scores aren't inflated by _that_ much compared to others. + */ + + decimal objectToDrainRatio = drainLength != 0 + ? Math.Clamp((decimal)objectCount / drainLength * 8, 0, 16) + : 16; + + /* + * Notably, THE `double` CASTS BELOW ARE IMPORTANT AND MUST REMAIN. + * Their goal is to trick the compiler / runtime into NOT promoting from single-precision float, as doing so would prompt it + * to attempt to "silently" fix the single-precision values when converting to decimal, + * which is NOT what the x87 FPU does. + */ + + decimal drainRate = (decimal)(double)difficulty.DrainRate; + decimal overallDifficulty = (decimal)(double)difficulty.OverallDifficulty; + decimal circleSize = (decimal)(double)difficulty.CircleSize; + + return (int)Math.Round((drainRate + overallDifficulty + circleSize + objectToDrainRatio) / 38 * 5); + } } } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 110cf63e5c..389b20b5c8 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -13,6 +13,7 @@ using osu.Game.Extensions; using osu.Game.IO.Legacy; using osu.Game.IO.Serialization; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using SharpCompress.Compressors.LZMA; @@ -38,9 +39,13 @@ namespace osu.Game.Scoring.Legacy /// 30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores. /// 30000010: Fix mania score V1 conversion using score V1 accuracy rather than V2 accuracy. Reconvert all scores. /// 30000011: Re-do catch scoring to mirror stable Score V2 as closely as feasible. Reconvert all scores. + /// + /// 30000012: Fix incorrect total score conversion on selected beatmaps after implementing the more correct + /// method. Reconvert all scores. + /// /// /// - public const int LATEST_VERSION = 30000011; + public const int LATEST_VERSION = 30000012; /// /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.