Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reset mod multiplier to 1 for classic mod in rulesets other than osu! #27816

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableHealthProcessor
{
public override double ScoreMultiplier => 0.96;

public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModStrictTracking)).ToArray();

[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
Expand Down
59 changes: 59 additions & 0 deletions osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
Expand Down Expand Up @@ -211,6 +212,64 @@ public void TestCustomRulesetScoreNotSubjectToUpgrades([Values] bool available)
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001));
}

[Test]
public void TestClassicModMultiplierChange()
{
AddStep("Add scores", () =>
{
string[] rulesets = ["osu", "taiko", "fruits", "mania"];

foreach (string ruleset in rulesets)
{
Realm.Write(r =>
{
r.Add(new ScoreInfo(ruleset: r.Find<RulesetInfo>(ruleset), beatmap: r.All<BeatmapInfo>().First())
{
TotalScoreVersion = 30000016,
TotalScore = 960_000,
APIMods = [new APIMod { Acronym = "CL" }]
});
});
}
});

AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));

AddUntilStep("Three scores updated", () => Realm.Run(r => r.All<ScoreInfo>().Count(score => score.TotalScore == 1_000_000)), () => Is.EqualTo(3));
AddUntilStep("osu! score preserved", () => Realm.Run(r => r.All<ScoreInfo>().Count(score => score.TotalScore == 960_000)), () => Is.EqualTo(1));
AddAssert("No fails", () => Realm.Run(r => r.All<ScoreInfo>().Count(score => score.BackgroundReprocessingFailed)), () => Is.Zero);
AddUntilStep("Score versions upgraded", () => Realm.Run(r => r.All<ScoreInfo>().Count(score => score.TotalScoreVersion >= 30000017)), () => Is.EqualTo(3));
}

[Test]
public void TestMultipleMigrationsAtOnce()
{
Guid scoreId = Guid.Empty;

AddStep("Add score", () =>
{
Realm.Write(r =>
{
var added = r.Add(new ScoreInfo(ruleset: r.Find<RulesetInfo>("taiko"), beatmap: r.All<BeatmapInfo>().First())
{
TotalScoreVersion = 30000010,
TotalScore = 960_000,
Accuracy = 0.91,
Rank = ScoreRank.B,
APIMods = [new APIMod { Acronym = "CL" }]
});
scoreId = added.ID;
});
});

AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));

AddUntilStep("Score total updated", () => Realm.Run(r => r.Find<ScoreInfo>(scoreId))!.TotalScore, () => Is.EqualTo(1_000_000));
AddUntilStep("Score total updated", () => Realm.Run(r => r.Find<ScoreInfo>(scoreId))!.Rank, () => Is.EqualTo(ScoreRank.A));
AddAssert("Upgrade did not fail", () => Realm.Run(r => r.Find<ScoreInfo>(scoreId))!.BackgroundReprocessingFailed, () => Is.False);
AddUntilStep("All score versions upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreId))!.TotalScoreVersion, () => Is.GreaterThanOrEqualTo(30000017));
}

public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
{
protected override int TimeToSleepDuringGameplay => 10;
Expand Down
158 changes: 153 additions & 5 deletions osu.Game/Database/BackgroundDataStoreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,16 @@ protected override void LoadComplete()
// Note that the previous method will also update these on a fresh run.
processBeatmapsWithMissingObjectCounts();
processScoresWithMissingStatistics();

convertLegacyTotalScoreToStandardised();
upgradeScoreRanks();

var upgradedScoreIds = new HashSet<Guid>();

upgradedScoreIds.UnionWith(upgradeScoreRanks());
upgradedScoreIds.UnionWith(upgradeModMultipliers());

if (upgradedScoreIds.Any())
bumpScoreVersions();
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
{
if (t.Exception?.InnerException is ObjectDisposedException)
Expand Down Expand Up @@ -322,7 +330,7 @@ private void convertLegacyTotalScoreToStandardised()
.Where(s => !s.BackgroundReprocessingFailed
&& s.BeatmapInfo != null
&& s.IsLegacyScore
&& s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
&& s.TotalScoreVersion < 30000016) // last total score version associated with changes to the score estimation algorithm
.AsEnumerable()
// must be done after materialisation, as realm doesn't want to support
// nested property predicates
Expand Down Expand Up @@ -356,6 +364,9 @@ private void convertLegacyTotalScoreToStandardised()
{
ScoreInfo s = r.Find<ScoreInfo>(id)!;
StandardisedScoreMigrationTools.UpdateFromLegacy(s, beatmapManager.GetWorkingBeatmap(s.BeatmapInfo));
// this intentionally sets the latest version here, rather than go through the `bumpScoreVersion()` flow.
// the reason is that `UpdateFromLegacy()` always recalculates both total score and score rank using latest lazer logic,
// so those migrations would be redundant or even straight-up wrong if applied on top of this one.
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
});

Expand All @@ -376,7 +387,7 @@ private void convertLegacyTotalScoreToStandardised()
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
}

private void upgradeScoreRanks()
private HashSet<Guid> upgradeScoreRanks()
{
Logger.Log("Querying for scores that need rank upgrades...");

Expand All @@ -392,7 +403,7 @@ private void upgradeScoreRanks()
Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades.");

if (scoreIds.Count == 0)
return;
return scoreIds;

var notification = showProgressNotification(scoreIds.Count, "Adjusting ranks of scores", "scores now have more correct ranks");

Expand All @@ -416,7 +427,6 @@ private void upgradeScoreRanks()
{
ScoreInfo s = r.Find<ScoreInfo>(id)!;
s.Rank = StandardisedScoreMigrationTools.ComputeRank(s);
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
});

++processedCount;
Expand All @@ -434,6 +444,144 @@ private void upgradeScoreRanks()
}

completeNotification(notification, processedCount, scoreIds.Count, failedCount);
return scoreIds;
}

private record ModMultiplierChange(
string ModAcronym,
string RulesetName,
double OldMultiplier,
double NewMultiplier,
int ScoreVersion);

private static readonly ModMultiplierChange[] mod_multiplier_changes =
[
new ModMultiplierChange("CL", "taiko", 0.96, 1, 30000017),
new ModMultiplierChange("CL", "fruits", 0.96, 1, 30000017),
new ModMultiplierChange("CL", "mania", 0.96, 1, 30000017),
];

private HashSet<Guid> upgradeModMultipliers()
{
Logger.Log("Performing mod multiplier upgrades...");
var upgradedScoreIds = new HashSet<Guid>();

int latestMultiplierChange = mod_multiplier_changes.Max(change => change.ScoreVersion);

var scores = realmAccess
.Run(r => r.All<ScoreInfo>()
.Where(score => score.TotalScoreVersion < latestMultiplierChange && !score.BackgroundReprocessingFailed)
.Detach())
// must be done after materialisation, as realm doesn't support
// filtering on nested property predicates or projection via `.Select()`
.Where(s => s.Ruleset.IsLegacyRuleset())
.ToList();

if (scores.Count == 0)
return upgradedScoreIds;

var notification = showProgressNotification(scores.Count, "Adjusting mod multipliers for scores", "scores now use latest mod multipliers");

int processedCount = 0;
int failedCount = 0;

foreach (var score in scores)
{
if (notification?.State == ProgressNotificationState.Cancelled)
break;

updateNotificationProgress(notification, processedCount, scores.Count);

sleepIfRequired();

double? newTotalScore = null;

try
{
foreach (var multiplierChange in mod_multiplier_changes)
{
if (score.TotalScoreVersion >= multiplierChange.ScoreVersion
|| score.Ruleset.ShortName != multiplierChange.RulesetName
|| score.APIMods.All(m => m.Acronym != multiplierChange.ModAcronym))
{
continue;
}

newTotalScore ??= score.TotalScore;
newTotalScore /= multiplierChange.OldMultiplier;
newTotalScore *= multiplierChange.NewMultiplier;
}

if (newTotalScore != null)
{
realmAccess.Write(r =>
{
var refetched = r.Find<ScoreInfo>(score.ID)!;
refetched.TotalScore = (long)newTotalScore.Value;
});
upgradedScoreIds.Add(score.ID);
}

++processedCount;
}
catch (ObjectDisposedException)
{
throw;
}
catch (Exception e)
{
Logger.Log($"Failed to update rank score {score.ID}: {e}");
realmAccess.Write(r => r.Find<ScoreInfo>(score.ID)!.BackgroundReprocessingFailed = true);
++failedCount;
}
}

completeNotification(notification, processedCount, scores.Count, failedCount);
return upgradedScoreIds;
}

private void bumpScoreVersions()
{
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(
r.All<ScoreInfo>()
.Where(s => s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
.AsEnumerable()
// must be done after materialisation, as realm doesn't support
// filtering on nested property predicates or projection via `.Select()`
.Where(s => s.Ruleset.IsLegacyRuleset())
.Select(s => s.ID)));

if (!scoreIds.Any())
return;

Logger.Log($"Updating score version for {scoreIds.Count} scores...");

foreach (var id in scoreIds)
{
sleepIfRequired();

try
{
// Can't use async overload because we're not on the update thread.
// ReSharper disable once MethodHasAsyncOverload
realmAccess.Write(r =>
{
ScoreInfo s = r.Find<ScoreInfo>(id)!;
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
});
}
catch (ObjectDisposedException)
{
throw;
}
catch (Exception e)
{
Logger.Log($"Failed to bump score version {id}: {e}");
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
}
}

Logger.Log("Finished updating score versions.");
}

private void updateNotificationProgress(ProgressNotification? notification, int processedCount, int totalCount)
Expand Down
2 changes: 1 addition & 1 deletion osu.Game/Rulesets/Mods/ModClassic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public abstract class ModClassic : Mod

public override string Acronym => "CL";

public override double ScoreMultiplier => 0.96;
public override double ScoreMultiplier => 1;

public override IconUsage? Icon => FontAwesome.Solid.History;

Expand Down
3 changes: 2 additions & 1 deletion osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ public class LegacyScoreEncoder
/// <item><description>30000014: Fix edge cases in conversion for osu! scores on selected beatmaps. Reconvert all scores.</description></item>
/// <item><description>30000015: Fix osu! standardised score estimation algorithm violating basic invariants. Reconvert all scores.</description></item>
/// <item><description>30000016: Fix taiko standardised score estimation algorithm not including swell tick score gain into bonus portion. Reconvert all scores.</description></item>
/// <item><description>30000017: Change multiplier of classic mod from 0.96x to 1.00x on all rulesets except osu!. Reconvert all relevant scores.</description></item>
/// </list>
/// </remarks>
public const int LATEST_VERSION = 30000016;
public const int LATEST_VERSION = 30000017;

/// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
Expand Down
Loading