1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 20:03:22 +08:00

Add basic setup for score migration

This commit is contained in:
Dean Herbert 2023-06-13 01:08:51 +09:00
parent 226bb4f387
commit f30c1a564f
2 changed files with 149 additions and 1 deletions

View File

@ -28,7 +28,10 @@ using osu.Game.IO.Legacy;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Skinning;
@ -76,8 +79,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.
@ -930,6 +934,28 @@ 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.
long oldStandardised = StandardisedScoreMigrationTools.GetOldStandardised(score);
if (oldStandardised == score.TotalScore)
{
long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score);
Logger.Log($"Converting score {score.Rank} {score.Accuracy:P1} {score.TotalScore} -> {calculatedNew}");
score.TotalScore = calculatedNew;
}
}
break;
}
}
}
@ -1151,4 +1177,26 @@ namespace osu.Game.Database
}
}
}
internal class FakeHit : HitObject
{
private readonly Judgement judgement;
public override Judgement CreateJudgement() => judgement;
public FakeHit(Judgement judgement)
{
this.judgement = judgement;
}
}
internal class FakeJudgement : Judgement
{
public override HitResult MaxResult { get; }
public FakeJudgement(HitResult result)
{
MaxResult = result;
}
}
}

View File

@ -0,0 +1,100 @@
// 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.Diagnostics;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Database
{
public static class StandardisedScoreMigrationTools
{
public static long GetNewStandardised(ScoreInfo score)
{
var processor = score.Ruleset.CreateInstance().CreateScoreProcessor();
var beatmap = new Beatmap();
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();
// 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() && kvp.Key != HitResult.Miss && kvp.Key != HitResult.LargeTickMiss)
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))
.ToList();
foreach (var judgement in maximumJudgements)
beatmap.HitObjects.Add(new FakeHit(judgement));
processor.ApplyBeatmap(beatmap);
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)));
int maxJudgementIndex = 0;
foreach (var result in sortedHits)
{
if (processor.Combo.Value == score.MaxCombo)
{
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
{
Type = misses.Dequeue(),
});
}
// TODO: pass a Judgement with correct MaxResult
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
{
Type = result
});
}
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 });
Debug.Assert(processor.HighestCombo.Value == score.MaxCombo);
return processor.TotalScore.Value;
}
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;
}
return (long)(1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore);
}
}
}