mirror of
https://github.com/ppy/osu
synced 2024-12-30 02:42:29 +00:00
6266af8a56
Reported in https://discord.com/channels/188630481301012481/1097318920991559880/1221836384038551613. Example score: https://osu.ppy.sh/scores/1855965185 The cause of the overestimation was an error in taiko's score simulator. In lazer taiko, swell ticks don't give any score anymore, while they did in stable. For all intents and purposes, swell ticks can be considered "bonus" objects that "don't give any actual bonus score". Which is to say, during simulation of a legacy score swell ticks hit should be treated as bonus, because if they aren't, then otherwise they will be treated essentially as *normal hits*, meaning that they will be included in the *accuracy* portion of score, which breaks all sorts of follow-up assumptions: - The accuracy portion of the best possible total score becomes overinflated in comparison to reality, while the combo portion of that maximum score becomes underestimated. - Because the affected score has low accuracy, the estimated accuracy portion of the score (as given by maximmum accuracy portion of score times the actual numerical accuracy of the score) is also low. - However, the next step is estimating the combo portion, which is done by taking legacy total score, subtracting the aforementioned estimation for accuracy total score from that, and then dividing the result by the maximum achievable combo score on the map. Because most of actual "combo" score from swell ticks was "moved" into the accuracy portion due to the aforementioned error, the maximum achievable combo score becomes so small that the estimated combo portion exceeds 1. Instead, this change makes it so that gains from swell ticks are treated as "bonus", which means that they are excluded from the accuracy portion of score and instead count into the bonus portion of score, bringing the scores concerned more in line with expectations - although due to pessimistic assumptions in the simulation of the swell itself, the conversion will still overestimate total score for affected scores, just not by *that* much.
247 lines
8.9 KiB
C#
247 lines
8.9 KiB
C#
// 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;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Rulesets.Mods;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Rulesets.Objects.Legacy;
|
|
using osu.Game.Rulesets.Objects.Types;
|
|
using osu.Game.Rulesets.Scoring;
|
|
using osu.Game.Rulesets.Scoring.Legacy;
|
|
using osu.Game.Rulesets.Taiko.Mods;
|
|
using osu.Game.Rulesets.Taiko.Objects;
|
|
using osu.Game.Rulesets.Taiko.Scoring;
|
|
|
|
namespace osu.Game.Rulesets.Taiko.Difficulty
|
|
{
|
|
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
|
|
{
|
|
private readonly ScoreProcessor scoreProcessor = new TaikoScoreProcessor();
|
|
|
|
private int legacyBonusScore;
|
|
private int standardisedBonusScore;
|
|
private int combo;
|
|
|
|
private int difficultyPeppyStars;
|
|
private IBeatmap playableBeatmap = null!;
|
|
|
|
public LegacyScoreAttributes Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap)
|
|
{
|
|
this.playableBeatmap = playableBeatmap;
|
|
|
|
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
|
|
|
int countNormal = 0;
|
|
int countSlider = 0;
|
|
int countSpinner = 0;
|
|
|
|
foreach (HitObject obj in baseBeatmap.HitObjects)
|
|
{
|
|
switch (obj)
|
|
{
|
|
case IHasPath:
|
|
countSlider++;
|
|
break;
|
|
|
|
case IHasDuration:
|
|
countSpinner++;
|
|
break;
|
|
|
|
default:
|
|
countNormal++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int objectCount = countNormal + countSlider + countSpinner;
|
|
|
|
int drainLength = 0;
|
|
|
|
if (baseBeatmap.HitObjects.Count > 0)
|
|
{
|
|
int breakLength = baseBeatmap.Breaks.Select(b => (int)Math.Round(b.EndTime) - (int)Math.Round(b.StartTime)).Sum();
|
|
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
|
}
|
|
|
|
difficultyPeppyStars = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
|
|
|
|
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
|
|
|
foreach (var obj in playableBeatmap.HitObjects)
|
|
simulateHit(obj, ref attributes);
|
|
|
|
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
|
attributes.BonusScore = legacyBonusScore;
|
|
attributes.MaxCombo = combo;
|
|
|
|
return attributes;
|
|
}
|
|
|
|
private void simulateHit(HitObject hitObject, ref LegacyScoreAttributes attributes)
|
|
{
|
|
bool increaseCombo = true;
|
|
bool addScoreComboMultiplier = false;
|
|
|
|
bool isBonus = false;
|
|
HitResult bonusResult = HitResult.None;
|
|
|
|
int scoreIncrease = 0;
|
|
|
|
switch (hitObject)
|
|
{
|
|
case SwellTick:
|
|
scoreIncrease = 300;
|
|
increaseCombo = false;
|
|
isBonus = true;
|
|
bonusResult = HitResult.IgnoreHit;
|
|
break;
|
|
|
|
case DrumRollTick:
|
|
scoreIncrease = 300;
|
|
increaseCombo = false;
|
|
isBonus = true;
|
|
bonusResult = HitResult.SmallBonus;
|
|
break;
|
|
|
|
case Swell swell:
|
|
// The taiko swell generally does not match the osu-stable implementation in any way.
|
|
// We'll redo the calculations to match osu-stable here...
|
|
|
|
// Normally, this value depends on the final overall difficulty. For simplicity, we'll only consider the worst case that maximises rotations.
|
|
const double minimum_rotations_per_second = 7.5;
|
|
|
|
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
|
int halfSpinsRequiredForCompletion = (int)(swell.Duration / 1000 * minimum_rotations_per_second);
|
|
halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f);
|
|
|
|
//
|
|
// Normally, this multiplier depends on the active mods (DT = 0.75, HT = 1.5). For simplicity, we'll only consider the worst case that maximises rotations.
|
|
// This way, scores remain beatable at the cost of the conversion being slightly inaccurate.
|
|
// - A perfect DT/NM score will have less than 1M total score (excluding bonus).
|
|
// - A perfect HT score will have 1M total score (excluding bonus).
|
|
//
|
|
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f));
|
|
|
|
for (int i = 0; i <= halfSpinsRequiredForCompletion; i++)
|
|
simulateHit(new SwellTick(), ref attributes);
|
|
|
|
scoreIncrease = 300;
|
|
addScoreComboMultiplier = true;
|
|
increaseCombo = false;
|
|
isBonus = true;
|
|
bonusResult = HitResult.LargeBonus;
|
|
break;
|
|
|
|
case Hit:
|
|
scoreIncrease = 300;
|
|
addScoreComboMultiplier = true;
|
|
break;
|
|
|
|
case DrumRoll:
|
|
foreach (var nested in hitObject.NestedHitObjects)
|
|
simulateHit(nested, ref attributes);
|
|
return;
|
|
}
|
|
|
|
if (hitObject is DrumRollTick tick)
|
|
{
|
|
if (playableBeatmap.ControlPointInfo.EffectPointAt(tick.Parent.StartTime).KiaiMode)
|
|
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
|
|
|
if (tick.IsStrong)
|
|
scoreIncrease += scoreIncrease / 5;
|
|
}
|
|
|
|
// The score increase directly contributed to by the combo-multiplied portion.
|
|
int comboScoreIncrease = 0;
|
|
|
|
if (addScoreComboMultiplier)
|
|
{
|
|
int oldScoreIncrease = scoreIncrease;
|
|
|
|
scoreIncrease += scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * (Math.Min(100, combo) / 10);
|
|
|
|
if (hitObject is Swell)
|
|
{
|
|
if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.GetEndTime()).KiaiMode)
|
|
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
|
}
|
|
else
|
|
{
|
|
if (playableBeatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)
|
|
scoreIncrease = (int)(scoreIncrease * 1.2f);
|
|
}
|
|
|
|
comboScoreIncrease = scoreIncrease - oldScoreIncrease;
|
|
}
|
|
|
|
if (hitObject is Swell || (hitObject is TaikoStrongableHitObject strongable && strongable.IsStrong))
|
|
{
|
|
scoreIncrease *= 2;
|
|
comboScoreIncrease *= 2;
|
|
}
|
|
|
|
scoreIncrease -= comboScoreIncrease;
|
|
|
|
if (addScoreComboMultiplier)
|
|
attributes.ComboScore += comboScoreIncrease;
|
|
|
|
if (isBonus)
|
|
{
|
|
legacyBonusScore += scoreIncrease;
|
|
standardisedBonusScore += scoreProcessor.GetBaseScoreForResult(bonusResult);
|
|
}
|
|
else
|
|
attributes.AccuracyScore += scoreIncrease;
|
|
|
|
if (increaseCombo)
|
|
combo++;
|
|
}
|
|
|
|
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
|
|
{
|
|
bool scoreV2 = mods.Any(m => m is ModScoreV2);
|
|
|
|
double multiplier = 1.0;
|
|
|
|
foreach (var mod in mods)
|
|
{
|
|
switch (mod)
|
|
{
|
|
case TaikoModNoFail:
|
|
multiplier *= scoreV2 ? 1.0 : 0.5;
|
|
break;
|
|
|
|
case TaikoModEasy:
|
|
multiplier *= 0.5;
|
|
break;
|
|
|
|
case TaikoModHalfTime:
|
|
case TaikoModDaycore:
|
|
multiplier *= 0.3;
|
|
break;
|
|
|
|
case TaikoModHidden:
|
|
case TaikoModHardRock:
|
|
multiplier *= 1.06;
|
|
break;
|
|
|
|
case TaikoModDoubleTime:
|
|
case TaikoModNightcore:
|
|
case TaikoModFlashlight:
|
|
multiplier *= 1.12;
|
|
break;
|
|
|
|
case TaikoModRelax:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
return multiplier;
|
|
}
|
|
}
|
|
}
|