// Copyright (c) ppy Pty Ltd . 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) { // 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; 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 sortedHits = score.Statistics .Where(kvp => kvp.Key.AffectsCombo()) .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key)) .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)) .ToList(); if (maximumJudgements.Count != sortedHits.Count) { // Older scores may not have maximum judgements populated correctly. // In this case we need to fill them. maximumJudgements = sortedHits .Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement))) .ToList(); } foreach (var judgement in maximumJudgements) beatmap.HitObjects.Add(new FakeHit(judgement)); processor.ApplyBeatmap(beatmap); Queue misses = new Queue(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) { // misses are handled from the queue. if (result == HitResult.Miss || result == HitResult.LargeTickMiss) continue; if (processor.Combo.Value == maxCombo) { if (misses.Count > 0) { processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++]) { Type = misses.Dequeue(), }); } else { // worst case scenario, insert a miss. processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement))) { Type = HitResult.Miss, }); } } 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 }); // Not true for all scores for whatever reason. Oh well. // Debug.Assert(processor.HighestCombo.Value == score.MaxCombo); return processor.TotalScore.Value; } 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; } return (long)(1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore); } 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 result) { MaxResult = result; } } } }