mirror of
https://github.com/ppy/osu.git
synced 2024-11-13 18:07:25 +08:00
Merge pull request #23892 from peppy/local-score-recalc
Migrate old standardised scores to new algorithm
This commit is contained in:
commit
74126524cf
@ -76,8 +76,9 @@ namespace osu.Game.Database
|
||||
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
|
||||
/// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo.
|
||||
/// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
|
||||
/// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes.
|
||||
/// </summary>
|
||||
private const int schema_version = 28;
|
||||
private const int schema_version = 29;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@ -724,6 +725,11 @@ namespace osu.Game.Database
|
||||
|
||||
private void applyMigrationsForVersion(Migration migration, ulong targetVersion)
|
||||
{
|
||||
Logger.Log($"Running realm migration to version {targetVersion}...");
|
||||
Stopwatch stopwatch = new Stopwatch();
|
||||
|
||||
stopwatch.Start();
|
||||
|
||||
switch (targetVersion)
|
||||
{
|
||||
case 7:
|
||||
@ -930,7 +936,38 @@ namespace osu.Game.Database
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 29:
|
||||
{
|
||||
var scores = migration.NewRealm
|
||||
.All<ScoreInfo>()
|
||||
.Where(s => !s.IsLegacyScore);
|
||||
|
||||
foreach (var score in scores)
|
||||
{
|
||||
// Recalculate the old-style standardised score to see if this was an old lazer score.
|
||||
bool oldScoreMatchesExpectations = StandardisedScoreMigrationTools.GetOldStandardised(score) == score.TotalScore;
|
||||
// Some older scores don't have correct statistics populated, so let's give them benefit of doubt.
|
||||
bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0);
|
||||
|
||||
if (oldScoreMatchesExpectations || scoreIsVeryOld)
|
||||
{
|
||||
try
|
||||
{
|
||||
long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score);
|
||||
score.TotalScore = calculatedNew;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
private string? getRulesetShortNameFromLegacyID(long rulesetId)
|
||||
|
196
osu.Game/Database/StandardisedScoreMigrationTools.cs
Normal file
196
osu.Game/Database/StandardisedScoreMigrationTools.cs
Normal file
@ -0,0 +1,196 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public static class StandardisedScoreMigrationTools
|
||||
{
|
||||
public static long GetNewStandardised(ScoreInfo score)
|
||||
{
|
||||
int maxJudgementIndex = 0;
|
||||
|
||||
// Avoid retrieving from realm inside loops.
|
||||
int maxCombo = score.MaxCombo;
|
||||
|
||||
var ruleset = score.Ruleset.CreateInstance();
|
||||
var processor = ruleset.CreateScoreProcessor();
|
||||
|
||||
processor.TrackHitEvents = false;
|
||||
|
||||
var beatmap = new Beatmap();
|
||||
|
||||
HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result;
|
||||
|
||||
// This is a list of all results, ordered from best to worst.
|
||||
// We are constructing a "best possible" score from the statistics provided because it's the best we can do.
|
||||
List<HitResult> sortedHits = score.Statistics
|
||||
.Where(kvp => kvp.Key.AffectsCombo())
|
||||
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
|
||||
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))
|
||||
.ToList();
|
||||
|
||||
// Attempt to use maximum statistics from the database.
|
||||
var maximumJudgements = score.MaximumStatistics
|
||||
.Where(kvp => kvp.Key.AffectsCombo())
|
||||
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
|
||||
.SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value))
|
||||
.ToList();
|
||||
|
||||
// Some older scores may not have maximum statistics populated correctly.
|
||||
// In this case we need to fill them with best-known-defaults.
|
||||
if (maximumJudgements.Count != sortedHits.Count)
|
||||
{
|
||||
maximumJudgements = sortedHits
|
||||
.Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// This is required to get the correct maximum combo portion.
|
||||
foreach (var judgement in maximumJudgements)
|
||||
beatmap.HitObjects.Add(new FakeHit(judgement));
|
||||
processor.ApplyBeatmap(beatmap);
|
||||
processor.Mods.Value = score.Mods;
|
||||
|
||||
// Insert all misses into a queue.
|
||||
// These will be nibbled at whenever we need to reset the combo.
|
||||
Queue<HitResult> misses = new Queue<HitResult>(score.Statistics
|
||||
.Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss)
|
||||
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)));
|
||||
|
||||
foreach (var result in sortedHits)
|
||||
{
|
||||
// For the main part of this loop, ignore all misses, as they will be inserted from the queue.
|
||||
if (result == HitResult.Miss || result == HitResult.LargeTickMiss)
|
||||
continue;
|
||||
|
||||
// Reset combo if required.
|
||||
if (processor.Combo.Value == maxCombo)
|
||||
insertMiss();
|
||||
|
||||
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
|
||||
{
|
||||
Type = result
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure we haven't forgotten any misses.
|
||||
while (misses.Count > 0)
|
||||
insertMiss();
|
||||
|
||||
var bonusHits = score.Statistics
|
||||
.Where(kvp => kvp.Key.IsBonus())
|
||||
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value));
|
||||
|
||||
foreach (var result in bonusHits)
|
||||
processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(result)) { Type = result });
|
||||
|
||||
// Not true for all scores for whatever reason. Oh well.
|
||||
// Debug.Assert(processor.HighestCombo.Value == score.MaxCombo);
|
||||
|
||||
return processor.TotalScore.Value;
|
||||
|
||||
void insertMiss()
|
||||
{
|
||||
if (misses.Count > 0)
|
||||
{
|
||||
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
|
||||
{
|
||||
Type = misses.Dequeue(),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// We ran out of misses. But we can't let max combo increase beyond the known value,
|
||||
// so let's forge a miss.
|
||||
processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement)))
|
||||
{
|
||||
Type = HitResult.Miss,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static HitResult getMaxJudgementFor(HitResult hitResult, HitResult max)
|
||||
{
|
||||
switch (hitResult)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
case HitResult.Meh:
|
||||
case HitResult.Ok:
|
||||
case HitResult.Good:
|
||||
case HitResult.Great:
|
||||
case HitResult.Perfect:
|
||||
return max;
|
||||
|
||||
case HitResult.SmallTickMiss:
|
||||
case HitResult.SmallTickHit:
|
||||
return HitResult.SmallTickHit;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.LargeTickHit:
|
||||
return HitResult.LargeTickHit;
|
||||
}
|
||||
|
||||
return HitResult.IgnoreHit;
|
||||
}
|
||||
|
||||
public static long GetOldStandardised(ScoreInfo score)
|
||||
{
|
||||
double accuracyScore =
|
||||
(double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value)
|
||||
/ score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
|
||||
double comboScore = (double)score.MaxCombo / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
|
||||
double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
|
||||
|
||||
double accuracyPortion = 0.3;
|
||||
|
||||
switch (score.RulesetID)
|
||||
{
|
||||
case 1:
|
||||
accuracyPortion = 0.75;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
accuracyPortion = 0.99;
|
||||
break;
|
||||
}
|
||||
|
||||
double modMultiplier = 1;
|
||||
|
||||
foreach (var mod in score.Mods)
|
||||
modMultiplier *= mod.ScoreMultiplier;
|
||||
|
||||
return (long)((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
|
||||
}
|
||||
|
||||
private class FakeHit : HitObject
|
||||
{
|
||||
private readonly Judgement judgement;
|
||||
|
||||
public override Judgement CreateJudgement() => judgement;
|
||||
|
||||
public FakeHit(Judgement judgement)
|
||||
{
|
||||
this.judgement = judgement;
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
|
||||
public FakeJudgement(HitResult maxResult)
|
||||
{
|
||||
MaxResult = maxResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
protected int MaxHits { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="SimulateAutoplay"/> is currently running.
|
||||
/// </summary>
|
||||
protected bool IsSimulating { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total number of judged <see cref="HitObject"/>s at the current point in time.
|
||||
/// </summary>
|
||||
@ -146,6 +151,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> to simulate.</param>
|
||||
protected virtual void SimulateAutoplay(IBeatmap beatmap)
|
||||
{
|
||||
IsSimulating = true;
|
||||
|
||||
foreach (var obj in beatmap.HitObjects)
|
||||
simulate(obj);
|
||||
|
||||
@ -163,6 +170,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
result.Type = GetSimulatedHitResult(judgement);
|
||||
ApplyResult(result);
|
||||
}
|
||||
|
||||
IsSimulating = false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -30,6 +30,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
private const double accuracy_cutoff_c = 0.7;
|
||||
private const double accuracy_cutoff_d = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="HitEvents"/> should be populated during application of results.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Should only be disabled for special cases.
|
||||
/// When disabled, <see cref="JudgementProcessor.RevertResult"/> cannot be used.</remarks>
|
||||
internal bool TrackHitEvents = true;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
|
||||
/// </summary>
|
||||
@ -226,10 +234,16 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
ApplyScoreChange(result);
|
||||
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
if (!IsSimulating)
|
||||
{
|
||||
if (TrackHitEvents)
|
||||
{
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
}
|
||||
|
||||
updateScore();
|
||||
updateScore();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -242,6 +256,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
protected sealed override void RevertResultInternal(JudgementResult result)
|
||||
{
|
||||
if (!TrackHitEvents)
|
||||
throw new InvalidOperationException(@$"Rewind is not supported when {nameof(TrackHitEvents)} is disabled.");
|
||||
|
||||
Combo.Value = result.ComboAtJudgement;
|
||||
HighestCombo.Value = result.HighestComboAtJudgement;
|
||||
|
||||
@ -311,6 +328,9 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <param name="storeResults">Whether to store the current state of the <see cref="ScoreProcessor"/> for future use.</param>
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
// Run one last time to store max values.
|
||||
updateScore();
|
||||
|
||||
base.Reset(storeResults);
|
||||
|
||||
hitEvents.Clear();
|
||||
|
Loading…
Reference in New Issue
Block a user