1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 13:37:25 +08:00
osu-lazer/osu.Game/Database/StandardisedScoreMigrationTools.cs

298 lines
13 KiB
C#
Raw Normal View History

2023-06-13 00:08:51 +08:00
// 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;
2023-06-13 00:08:51 +08:00
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
2023-06-28 14:04:13 +08:00
using osu.Game.Rulesets.Difficulty;
2023-06-13 00:08:51 +08:00
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
2023-06-13 00:08:51 +08:00
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Database
{
public static class StandardisedScoreMigrationTools
{
public static bool ShouldMigrateToNewStandardised(ScoreInfo score)
{
if (score.IsLegacyScore)
return false;
// Recalculate the old-style standardised score to see if this was an old lazer score.
bool oldScoreMatchesExpectations = 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);
return oldScoreMatchesExpectations || scoreIsVeryOld;
}
2023-06-13 00:08:51 +08:00
public static long GetNewStandardised(ScoreInfo score)
{
2023-06-13 04:24:04 +08:00
int maxJudgementIndex = 0;
// Avoid retrieving from realm inside loops.
int maxCombo = score.MaxCombo;
var ruleset = score.Ruleset.CreateInstance();
var processor = ruleset.CreateScoreProcessor();
2023-06-13 00:08:51 +08:00
processor.TrackHitEvents = false;
2023-06-13 00:08:51 +08:00
var beatmap = new Beatmap();
HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result;
2023-06-13 00:08:51 +08:00
// 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())
2023-06-13 00:08:51 +08:00
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))
.ToList();
2023-06-13 04:20:29 +08:00
// 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();
}
2023-06-13 04:20:29 +08:00
// This is required to get the correct maximum combo portion.
2023-06-13 00:08:51 +08:00
foreach (var judgement in maximumJudgements)
beatmap.HitObjects.Add(new FakeHit(judgement));
processor.ApplyBeatmap(beatmap);
processor.Mods.Value = score.Mods;
2023-06-13 00:08:51 +08:00
2023-06-13 04:20:29 +08:00
// Insert all misses into a queue.
// These will be nibbled at whenever we need to reset the combo.
2023-06-13 00:08:51 +08:00
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)
{
2023-06-13 04:20:29 +08:00
// 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;
2023-06-13 04:24:04 +08:00
// Reset combo if required.
if (processor.Combo.Value == maxCombo)
2023-06-13 04:24:04 +08:00
insertMiss();
2023-06-13 00:08:51 +08:00
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
{
Type = result
});
}
2023-06-13 04:24:04 +08:00
// Ensure we haven't forgotten any misses.
while (misses.Count > 0)
insertMiss();
2023-06-13 00:08:51 +08:00
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);
2023-06-13 00:08:51 +08:00
return processor.TotalScore.Value;
2023-06-13 04:24:04 +08:00
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,
});
}
}
2023-06-13 00:08:51 +08:00
}
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;
}
2023-06-13 00:08:51 +08:00
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)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
2023-06-13 00:08:51 +08:00
}
2023-06-28 14:04:13 +08:00
/// <summary>
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
/// </summary>
/// <param name="score">The score to convert the total score of.</param>
/// <param name="beatmaps">A <see cref="BeatmapManager"/> used for <see cref="WorkingBeatmap"/> lookups.</param>
/// <returns>The standardised total score.</returns>
public static long ConvertFromLegacyTotalScore(ScoreInfo score, BeatmapManager beatmaps)
{
if (!score.IsLegacyScore)
return score.TotalScore;
var beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo);
var ruleset = score.Ruleset.CreateInstance();
var sv1Processor = ruleset.CreateLegacyScoreProcessor();
if (sv1Processor == null)
return score.TotalScore;
sv1Processor.Simulate(beatmap, beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods), score.Mods);
return ConvertFromLegacyTotalScore(score, new DifficultyAttributes
{
LegacyAccuracyScore = sv1Processor.AccuracyScore,
LegacyComboScore = sv1Processor.ComboScore,
LegacyBonusScoreRatio = sv1Processor.BonusScoreRatio
});
}
/// <summary>
/// Converts from <see cref="ScoreInfo.LegacyTotalScore"/> to the new standardised scoring of <see cref="ScoreProcessor"/>.
/// </summary>
/// <param name="score">The score to convert the total score of.</param>
/// <param name="attributes">Difficulty attributes providing the legacy scoring values
/// (<see cref="DifficultyAttributes.LegacyAccuracyScore"/>, <see cref="DifficultyAttributes.LegacyComboScore"/>, and <see cref="DifficultyAttributes.LegacyBonusScoreRatio"/>)
/// for the beatmap which the score was set on.</param>
/// <returns>The standardised total score.</returns>
public static long ConvertFromLegacyTotalScore(ScoreInfo score, DifficultyAttributes attributes)
{
if (!score.IsLegacyScore)
return score.TotalScore;
int maximumLegacyAccuracyScore = attributes.LegacyAccuracyScore;
int maximumLegacyComboScore = attributes.LegacyComboScore;
double maximumLegacyBonusRatio = attributes.LegacyBonusScoreRatio;
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
// The part of total score that doesn't include bonus.
int maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
// The combo proportion is calculated as a proportion of maximumLegacyBaseScore.
double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore);
// The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore.
double bonusProportion = Math.Max(0, (score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio);
switch (score.Ruleset.OnlineID)
{
case 0:
return (long)Math.Round((
700000 * comboProportion
+ 300000 * Math.Pow(score.Accuracy, 10)
+ bonusProportion) * modMultiplier);
case 1:
return (long)Math.Round((
250000 * comboProportion
+ 750000 * Math.Pow(score.Accuracy, 3.6)
+ bonusProportion) * modMultiplier);
case 2:
return (long)Math.Round((
600000 * comboProportion
+ 400000 * score.Accuracy
+ bonusProportion) * modMultiplier);
case 3:
return (long)Math.Round((
990000 * comboProportion
+ 10000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
+ bonusProportion) * modMultiplier);
default:
return score.TotalScore;
}
}
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; }
2023-06-13 04:20:29 +08:00
public FakeJudgement(HitResult maxResult)
{
2023-06-13 04:20:29 +08:00
MaxResult = maxResult;
}
}
2023-06-13 00:08:51 +08:00
}
}