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

Store total score without mod multipliers to local database and to replays, and send it on score submission #27912

Merged
merged 7 commits into from
May 10, 2024
74 changes: 74 additions & 0 deletions osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,80 @@ public void AccuracyAndRankOfLazerScoreWithoutLegacyReplaySoloScoreInfoUsesBestE
});
}

[Test]
public void TestTotalScoreWithoutModsReadIfPresent()
{
var ruleset = new OsuRuleset().RulesetInfo;

var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
scoreInfo.Mods = new Mod[]
{
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.ClientVersion = "2023.1221.0";
scoreInfo.TotalScoreWithoutMods = 1_000_000;
scoreInfo.TotalScore = 1_020_000;

var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};

var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);

Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
});
}

[Test]
public void TestTotalScoreWithoutModsBackwardsPopulatedIfMissing()
{
var ruleset = new OsuRuleset().RulesetInfo;

var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
scoreInfo.Mods = new Mod[]
{
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.ClientVersion = "2023.1221.0";
scoreInfo.TotalScoreWithoutMods = 0;
scoreInfo.TotalScore = 1_020_000;

var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};

var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);

Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.TotalScoreWithoutMods, Is.EqualTo(1_000_000));
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(1_020_000));
});
}

private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();
Expand Down
9 changes: 8 additions & 1 deletion osu.Game/Database/RealmAccess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ public class RealmAccess : IDisposable
/// 38 2023-12-10 Add EndTimeObjectCount and TotalObjectCount to BeatmapInfo.
/// 39 2023-12-19 Migrate any EndTimeObjectCount and TotalObjectCount values of 0 to -1 to better identify non-calculated values.
/// 40 2023-12-21 Add ScoreInfo.Version to keep track of which build scores were set on.
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
/// </summary>
private const int schema_version = 40;
private const int schema_version = 41;

/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
Expand Down Expand Up @@ -1130,6 +1131,12 @@ void convertOnlineIDs<T>() where T : RealmObject
}

break;

case 41:
foreach (var score in migration.NewRealm.All<ScoreInfo>())
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score);

break;
}

Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
Expand Down
2 changes: 2 additions & 0 deletions osu.Game/Database/StandardisedScoreMigrationTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;

namespace osu.Game.Database
{
Expand Down Expand Up @@ -248,6 +249,7 @@ public static void UpdateFromLegacy(ScoreInfo score, WorkingBeatmap beatmap)
score.Accuracy = computeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor);
score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap);
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score);
}

/// <summary>
Expand Down
5 changes: 5 additions & 0 deletions osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public class SoloScoreInfo : IScoreInfo
[JsonProperty("total_score")]
public long TotalScore { get; set; }

[JsonProperty("total_score_without_mods")]
public long TotalScoreWithoutMods { get; set; }

[JsonProperty("accuracy")]
public double Accuracy { get; set; }

Expand Down Expand Up @@ -206,6 +209,7 @@ public ScoreInfo ToScoreInfo(Mod[] mods, IBeatmapInfo? beatmap = null)
Ruleset = new RulesetInfo { OnlineID = RulesetID },
Passed = Passed,
TotalScore = TotalScore,
TotalScoreWithoutMods = TotalScoreWithoutMods,
LegacyTotalScore = LegacyTotalScore,
Accuracy = Accuracy,
MaxCombo = MaxCombo,
Expand Down Expand Up @@ -239,6 +243,7 @@ public ScoreInfo ToScoreInfo(Mod[] mods, IBeatmapInfo? beatmap = null)
{
Rank = score.Rank,
TotalScore = score.TotalScore,
TotalScoreWithoutMods = score.TotalScoreWithoutMods,
Accuracy = score.Accuracy,
PP = score.PP,
MaxCombo = score.MaxCombo,
Expand Down
12 changes: 11 additions & 1 deletion osu.Game/Rulesets/Scoring/ScoreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ public partial class ScoreProcessor : JudgementProcessor
/// </summary>
public readonly BindableLong TotalScore = new BindableLong { MinValue = 0 };

/// <summary>
/// The total number of points awarded for the score without including mod multipliers.
/// </summary>
/// <remarks>
/// The purpose of this property is to enable future lossless rebalances of mod multipliers.
/// </remarks>
public readonly BindableLong TotalScoreWithoutMods = new BindableLong { MinValue = 0 };

/// <summary>
/// The current accuracy.
/// </summary>
Expand Down Expand Up @@ -363,7 +371,8 @@ private void updateScore()
double comboProgress = maximumComboPortion > 0 ? currentComboPortion / maximumComboPortion : 1;
double accuracyProcess = maximumAccuracyJudgementCount > 0 ? (double)currentAccuracyJudgementCount / maximumAccuracyJudgementCount : 1;

TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier);
TotalScoreWithoutMods.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion));
TotalScore.Value = (long)Math.Round(TotalScoreWithoutMods.Value * scoreMultiplier);
}

private void updateRank()
Expand Down Expand Up @@ -446,6 +455,7 @@ public virtual void PopulateScore(ScoreInfo score)
score.MaximumStatistics[result] = MaximumResultCounts.GetValueOrDefault(result);

// Populate total score after everything else.
score.TotalScoreWithoutMods = TotalScoreWithoutMods.Value;
score.TotalScore = TotalScore.Value;
}

Expand Down
4 changes: 4 additions & 0 deletions osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public class LegacyReplaySoloScoreInfo
[JsonProperty("user_id")]
public int UserID = -1;

[JsonProperty("total_score_without_mods")]
public long? TotalScoreWithoutMods { get; set; }

public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo
{
OnlineID = score.OnlineID,
Expand All @@ -55,6 +58,7 @@ public class LegacyReplaySoloScoreInfo
ClientVersion = score.ClientVersion,
Rank = score.Rank,
UserID = score.User.OnlineID,
TotalScoreWithoutMods = score.TotalScoreWithoutMods > 0 ? score.TotalScoreWithoutMods : null,
};
}
}
15 changes: 15 additions & 0 deletions osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ public Score Parse(Stream stream)
decodedRank = readScore.Rank;
if (readScore.UserID > 1)
score.ScoreInfo.RealmUser.OnlineID = readScore.UserID;

if (readScore.TotalScoreWithoutMods is long totalScoreWithoutMods)
score.ScoreInfo.TotalScoreWithoutMods = totalScoreWithoutMods;
else
PopulateTotalScoreWithoutMods(score.ScoreInfo);
});
}
}
Expand Down Expand Up @@ -244,6 +249,16 @@ public static void PopulateMaximumStatistics(ScoreInfo score, WorkingBeatmap wor
#pragma warning restore CS0618
}

public static void PopulateTotalScoreWithoutMods(ScoreInfo score)
{
double modMultiplier = 1;

foreach (var mod in score.Mods)
modMultiplier *= mod.ScoreMultiplier;

score.TotalScoreWithoutMods = (long)Math.Round(score.TotalScore / modMultiplier);
}

private void readLegacyReplay(Replay replay, StreamReader reader)
{
float lastTime = beatmapOffset;
Expand Down
11 changes: 11 additions & 0 deletions osu.Game/Scoring/ScoreInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,19 @@ public class ScoreInfo : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftD

public bool DeletePending { get; set; }

/// <summary>
/// The total number of points awarded for the score.
/// </summary>
public long TotalScore { get; set; }

/// <summary>
/// The total number of points awarded for the score without including mod multipliers.
/// </summary>
/// <remarks>
/// The purpose of this property is to enable future lossless rebalances of mod multipliers.
/// </remarks>
public long TotalScoreWithoutMods { get; set; }

/// <summary>
/// The version of processing applied to calculate total score as stored in the database.
/// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>,
Expand Down
Loading