mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 07:07:45 +08:00
Merge pull request #24072 from smoogipoo/diffcalc-total-scorev1
Add difficulty attributes to facilitate conversion from legacy score, and convert existing scores
This commit is contained in:
commit
49e5558e4f
@ -202,6 +202,8 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public int LegacyID => 2;
|
||||
|
||||
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new CatchLegacyScoreSimulator();
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
|
@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
||||
yield return (ATTRIB_ID_AIM, StarRating);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@ -36,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
StarRating = values[ATTRIB_ID_AIM];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
public override int Version => 20220701;
|
||||
|
||||
private readonly IWorkingBeatmap workingBeatmap;
|
||||
|
||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
workingBeatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
@ -38,13 +41,24 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
return new CatchDifficultyAttributes
|
||||
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||
{
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
||||
Mods = mods,
|
||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
|
||||
};
|
||||
|
||||
if (ComputeLegacyScoringValues)
|
||||
{
|
||||
CatchLegacyScoreSimulator sv1Simulator = new CatchLegacyScoreSimulator();
|
||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
|
142
osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
Normal file
142
osu.Game.Rulesets.Catch/Difficulty/CatchLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,142 @@
|
||||
// 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.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
internal class CatchLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
public int AccuracyScore { get; private set; }
|
||||
|
||||
public int ComboScore { get; private set; }
|
||||
|
||||
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int modernBonusScore;
|
||||
private int combo;
|
||||
|
||||
private double scoreMultiplier;
|
||||
|
||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
|
||||
foreach (var obj in playableBeatmap.HitObjects)
|
||||
simulateHit(obj);
|
||||
}
|
||||
|
||||
private void simulateHit(HitObject hitObject)
|
||||
{
|
||||
bool increaseCombo = true;
|
||||
bool addScoreComboMultiplier = false;
|
||||
|
||||
bool isBonus = false;
|
||||
HitResult bonusResult = HitResult.None;
|
||||
|
||||
int scoreIncrease = 0;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case TinyDroplet:
|
||||
scoreIncrease = 10;
|
||||
increaseCombo = false;
|
||||
break;
|
||||
|
||||
case Droplet:
|
||||
scoreIncrease = 100;
|
||||
break;
|
||||
|
||||
case Fruit:
|
||||
scoreIncrease = 300;
|
||||
addScoreComboMultiplier = true;
|
||||
increaseCombo = true;
|
||||
break;
|
||||
|
||||
case Banana:
|
||||
scoreIncrease = 1100;
|
||||
increaseCombo = false;
|
||||
isBonus = true;
|
||||
bonusResult = HitResult.LargeBonus;
|
||||
break;
|
||||
|
||||
case JuiceStream:
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
simulateHit(nested);
|
||||
return;
|
||||
|
||||
case BananaShower:
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
simulateHit(nested);
|
||||
return;
|
||||
}
|
||||
|
||||
if (addScoreComboMultiplier)
|
||||
{
|
||||
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||
}
|
||||
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
}
|
||||
else
|
||||
AccuracyScore += scoreIncrease;
|
||||
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
}
|
||||
}
|
@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
}
|
||||
@ -33,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
}
|
||||
|
@ -31,9 +31,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
public override int Version => 20220902;
|
||||
|
||||
private readonly IWorkingBeatmap workingBeatmap;
|
||||
|
||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
workingBeatmap = beatmap;
|
||||
|
||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
||||
}
|
||||
@ -46,15 +50,26 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
HitWindows hitWindows = new ManiaHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
return new ManiaDifficultyAttributes
|
||||
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||
{
|
||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||
Mods = mods,
|
||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
|
||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
|
||||
};
|
||||
|
||||
if (ComputeLegacyScoringValues)
|
||||
{
|
||||
ManiaLegacyScoreSimulator sv1Simulator = new ManiaLegacyScoreSimulator();
|
||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static int maxComboForObject(HitObject hitObject)
|
||||
|
@ -0,0 +1,28 @@
|
||||
// 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.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
internal class ManiaLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
public int AccuracyScore => 0;
|
||||
public int ComboScore { get; private set; }
|
||||
public double BonusScoreRatio => 0;
|
||||
|
||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
double multiplier = mods.Where(m => m is not (ModHidden or ModHardRock or ModDoubleTime or ModFlashlight or ManiaModFadeIn))
|
||||
.Select(m => m.ScoreMultiplier)
|
||||
.Aggregate(1.0, (c, n) => c * n);
|
||||
|
||||
ComboScore = (int)(1000000 * multiplier);
|
||||
}
|
||||
}
|
||||
}
|
@ -302,6 +302,8 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public int LegacyID => 3;
|
||||
|
||||
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new ManiaLegacyScoreSimulator();
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
||||
|
@ -93,7 +93,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
|
||||
if (ShouldSerializeFlashlightRating())
|
||||
@ -111,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||
|
@ -26,9 +26,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
public override int Version => 20220902;
|
||||
|
||||
private readonly IWorkingBeatmap workingBeatmap;
|
||||
|
||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
workingBeatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
@ -71,7 +74,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
||||
);
|
||||
|
||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
||||
double starRating = basePerformance > 0.00001
|
||||
? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
|
||||
: 0;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
@ -86,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
return new OsuDifficultyAttributes
|
||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
@ -103,6 +108,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
SliderCount = sliderCount,
|
||||
SpinnerCount = spinnerCount,
|
||||
};
|
||||
|
||||
if (ComputeLegacyScoringValues)
|
||||
{
|
||||
OsuLegacyScoreSimulator sv1Simulator = new OsuLegacyScoreSimulator();
|
||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
|
177
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs
Normal file
177
osu.Game.Rulesets.Osu/Difficulty/OsuLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,177 @@
|
||||
// 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.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
internal class OsuLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
public int AccuracyScore { get; private set; }
|
||||
|
||||
public int ComboScore { get; private set; }
|
||||
|
||||
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int modernBonusScore;
|
||||
private int combo;
|
||||
|
||||
private double scoreMultiplier;
|
||||
private IBeatmap playableBeatmap = null!;
|
||||
|
||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
this.playableBeatmap = playableBeatmap;
|
||||
|
||||
IBeatmap baseBeatmap = workingBeatmap.Beatmap;
|
||||
|
||||
int countNormal = 0;
|
||||
int countSlider = 0;
|
||||
int countSpinner = 0;
|
||||
|
||||
foreach (HitObject obj in workingBeatmap.Beatmap.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;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars * mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
|
||||
foreach (var obj in playableBeatmap.HitObjects)
|
||||
simulateHit(obj);
|
||||
}
|
||||
|
||||
private void simulateHit(HitObject hitObject)
|
||||
{
|
||||
bool increaseCombo = true;
|
||||
bool addScoreComboMultiplier = false;
|
||||
|
||||
bool isBonus = false;
|
||||
HitResult bonusResult = HitResult.None;
|
||||
|
||||
int scoreIncrease = 0;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case SliderHeadCircle:
|
||||
case SliderTailCircle:
|
||||
case SliderRepeat:
|
||||
scoreIncrease = 30;
|
||||
break;
|
||||
|
||||
case SliderTick:
|
||||
scoreIncrease = 10;
|
||||
break;
|
||||
|
||||
case SpinnerBonusTick:
|
||||
scoreIncrease = 1100;
|
||||
increaseCombo = false;
|
||||
isBonus = true;
|
||||
bonusResult = HitResult.LargeBonus;
|
||||
break;
|
||||
|
||||
case SpinnerTick:
|
||||
scoreIncrease = 100;
|
||||
increaseCombo = false;
|
||||
isBonus = true;
|
||||
bonusResult = HitResult.SmallBonus;
|
||||
break;
|
||||
|
||||
case HitCircle:
|
||||
scoreIncrease = 300;
|
||||
addScoreComboMultiplier = true;
|
||||
break;
|
||||
|
||||
case Slider:
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
simulateHit(nested);
|
||||
|
||||
scoreIncrease = 300;
|
||||
increaseCombo = false;
|
||||
addScoreComboMultiplier = true;
|
||||
break;
|
||||
|
||||
case Spinner spinner:
|
||||
// The spinner object applies a lenience because gameplay mechanics differ from osu-stable.
|
||||
// We'll redo the calculations to match osu-stable here...
|
||||
const double maximum_rotations_per_second = 477.0 / 60;
|
||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||
double secondsDuration = spinner.Duration / 1000;
|
||||
|
||||
// The total amount of half spins possible for the entire spinner.
|
||||
int totalHalfSpinsPossible = (int)(secondsDuration * maximum_rotations_per_second * 2);
|
||||
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||
// To be able to receive bonus points, the spinner must be rotated another 1.5 times.
|
||||
int halfSpinsRequiredBeforeBonus = halfSpinsRequiredForCompletion + 3;
|
||||
|
||||
for (int i = 0; i <= totalHalfSpinsPossible; i++)
|
||||
{
|
||||
if (i > halfSpinsRequiredBeforeBonus && (i - halfSpinsRequiredBeforeBonus) % 2 == 0)
|
||||
simulateHit(new SpinnerBonusTick());
|
||||
else if (i > 1 && i % 2 == 0)
|
||||
simulateHit(new SpinnerTick());
|
||||
}
|
||||
|
||||
scoreIncrease = 300;
|
||||
addScoreComboMultiplier = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (addScoreComboMultiplier)
|
||||
{
|
||||
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||
ComboScore += (int)(Math.Max(0, combo - 1) * (scoreIncrease / 25 * scoreMultiplier));
|
||||
}
|
||||
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
}
|
||||
else
|
||||
AccuracyScore += scoreIncrease;
|
||||
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
}
|
||||
}
|
@ -253,6 +253,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public int LegacyID => 0;
|
||||
|
||||
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new OsuLegacyScoreSimulator();
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
|
||||
private void addFlyingHit(HitType hitType)
|
||||
{
|
||||
var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
|
||||
var tick = new DrumRollTick(new DrumRoll()) { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current };
|
||||
|
||||
DrawableDrumRollTick h;
|
||||
DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
|
||||
|
@ -48,7 +48,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
}
|
||||
@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
}
|
||||
|
@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
public override int Version => 20220902;
|
||||
|
||||
private readonly IWorkingBeatmap workingBeatmap;
|
||||
|
||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
workingBeatmap = beatmap;
|
||||
}
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
@ -84,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
return new TaikoDifficultyAttributes
|
||||
TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
@ -95,6 +98,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||
};
|
||||
|
||||
if (ComputeLegacyScoringValues)
|
||||
{
|
||||
TaikoLegacyScoreSimulator sv1Simulator = new TaikoLegacyScoreSimulator();
|
||||
sv1Simulator.Simulate(workingBeatmap, beatmap, mods);
|
||||
attributes.LegacyAccuracyScore = sv1Simulator.AccuracyScore;
|
||||
attributes.LegacyComboScore = sv1Simulator.ComboScore;
|
||||
attributes.LegacyBonusScoreRatio = sv1Simulator.BonusScoreRatio;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
202
osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs
Normal file
202
osu.Game.Rulesets.Taiko/Difficulty/TaikoLegacyScoreSimulator.cs
Normal file
@ -0,0 +1,202 @@
|
||||
// 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.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
internal class TaikoLegacyScoreSimulator : ILegacyScoreSimulator
|
||||
{
|
||||
public int AccuracyScore { get; private set; }
|
||||
|
||||
public int ComboScore { get; private set; }
|
||||
|
||||
public double BonusScoreRatio => legacyBonusScore == 0 ? 0 : (double)modernBonusScore / legacyBonusScore;
|
||||
|
||||
private int legacyBonusScore;
|
||||
private int modernBonusScore;
|
||||
private int combo;
|
||||
|
||||
private double modMultiplier;
|
||||
private int difficultyPeppyStars;
|
||||
private IBeatmap playableBeatmap = null!;
|
||||
private IReadOnlyList<Mod> mods = null!;
|
||||
|
||||
public void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
this.playableBeatmap = playableBeatmap;
|
||||
this.mods = mods;
|
||||
|
||||
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 = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
modMultiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier);
|
||||
|
||||
foreach (var obj in playableBeatmap.HitObjects)
|
||||
simulateHit(obj);
|
||||
}
|
||||
|
||||
private void simulateHit(HitObject hitObject)
|
||||
{
|
||||
bool increaseCombo = true;
|
||||
bool addScoreComboMultiplier = false;
|
||||
|
||||
bool isBonus = false;
|
||||
HitResult bonusResult = HitResult.None;
|
||||
|
||||
int scoreIncrease = 0;
|
||||
|
||||
switch (hitObject)
|
||||
{
|
||||
case SwellTick:
|
||||
scoreIncrease = 300;
|
||||
increaseCombo = false;
|
||||
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...
|
||||
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(playableBeatmap.Difficulty.OverallDifficulty, 3, 5, 7.5);
|
||||
double secondsDuration = swell.Duration / 1000;
|
||||
|
||||
// The amount of half spins that are required to successfully complete the spinner (i.e. get a 300).
|
||||
int halfSpinsRequiredForCompletion = (int)(secondsDuration * minimumRotationsPerSecond);
|
||||
|
||||
halfSpinsRequiredForCompletion = (int)Math.Max(1, halfSpinsRequiredForCompletion * 1.65f);
|
||||
|
||||
if (mods.Any(m => m is ModDoubleTime))
|
||||
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 0.75f));
|
||||
if (mods.Any(m => m is ModHalfTime))
|
||||
halfSpinsRequiredForCompletion = Math.Max(1, (int)(halfSpinsRequiredForCompletion * 1.5f));
|
||||
|
||||
for (int i = 0; i <= halfSpinsRequiredForCompletion; i++)
|
||||
simulateHit(new SwellTick());
|
||||
|
||||
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);
|
||||
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;
|
||||
|
||||
// ReSharper disable once PossibleLossOfFraction (intentional to match osu-stable...)
|
||||
scoreIncrease += (int)(scoreIncrease / 35 * 2 * (difficultyPeppyStars + 1) * modMultiplier) * (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)
|
||||
ComboScore += comboScoreIncrease;
|
||||
|
||||
if (isBonus)
|
||||
{
|
||||
legacyBonusScore += scoreIncrease;
|
||||
modernBonusScore += Judgement.ToNumericResult(bonusResult);
|
||||
}
|
||||
else
|
||||
AccuracyScore += scoreIncrease;
|
||||
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
}
|
||||
}
|
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
AddNested(new DrumRollTick
|
||||
AddNested(new DrumRollTick(this)
|
||||
{
|
||||
FirstTick = first,
|
||||
TickSpacing = tickSpacing,
|
||||
|
@ -9,6 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
{
|
||||
public class DrumRollTick : TaikoStrongableHitObject
|
||||
{
|
||||
public readonly DrumRoll Parent;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is the first (initial) tick of the slider.
|
||||
/// </summary>
|
||||
@ -25,6 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
/// </summary>
|
||||
public double HitWindow => TickSpacing / 2;
|
||||
|
||||
public DrumRollTick(DrumRoll parent)
|
||||
{
|
||||
Parent = parent;
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
@ -197,6 +197,8 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public int LegacyID => 1;
|
||||
|
||||
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new TaikoLegacyScoreSimulator();
|
||||
|
||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
|
||||
|
@ -14,8 +14,11 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game
|
||||
@ -25,6 +28,9 @@ namespace osu.Game
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
@ -40,19 +46,23 @@ namespace osu.Game
|
||||
[Resolved]
|
||||
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private INotificationOverlay? notificationOverlay { get; set; }
|
||||
|
||||
protected virtual int TimeToSleepDuringGameplay => 30000;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Task.Run(() =>
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
Logger.Log("Beginning background beatmap processing..");
|
||||
checkForOutdatedStarRatings();
|
||||
processBeatmapSetsWithMissingMetrics();
|
||||
processScoresWithMissingStatistics();
|
||||
}).ContinueWith(t =>
|
||||
convertLegacyTotalScoreToStandardised();
|
||||
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
||||
{
|
||||
if (t.Exception?.InnerException is ObjectDisposedException)
|
||||
{
|
||||
@ -121,11 +131,7 @@ namespace osu.Game
|
||||
|
||||
foreach (var id in beatmapSetIds)
|
||||
{
|
||||
while (localUserPlayInfo?.IsPlaying.Value == true)
|
||||
{
|
||||
Logger.Log("Background processing sleeping due to active gameplay...");
|
||||
Thread.Sleep(TimeToSleepDuringGameplay);
|
||||
}
|
||||
sleepIfRequired();
|
||||
|
||||
realmAccess.Run(r =>
|
||||
{
|
||||
@ -166,11 +172,7 @@ namespace osu.Game
|
||||
|
||||
foreach (var id in scoreIds)
|
||||
{
|
||||
while (localUserPlayInfo?.IsPlaying.Value == true)
|
||||
{
|
||||
Logger.Log("Background processing sleeping due to active gameplay...");
|
||||
Thread.Sleep(TimeToSleepDuringGameplay);
|
||||
}
|
||||
sleepIfRequired();
|
||||
|
||||
try
|
||||
{
|
||||
@ -187,11 +189,98 @@ namespace osu.Game
|
||||
|
||||
Logger.Log($"Populated maximum statistics for score {id}");
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void convertLegacyTotalScoreToStandardised()
|
||||
{
|
||||
Logger.Log("Querying for scores that need total score conversion...");
|
||||
|
||||
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>().Where(s => s.TotalScoreVersion == 30000002).AsEnumerable().Select(s => s.ID)));
|
||||
|
||||
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
|
||||
|
||||
if (scoreIds.Count == 0)
|
||||
return;
|
||||
|
||||
ProgressNotification notification = new ProgressNotification { State = ProgressNotificationState.Active };
|
||||
|
||||
notificationOverlay?.Post(notification);
|
||||
|
||||
int processedCount = 0;
|
||||
int failedCount = 0;
|
||||
|
||||
foreach (var id in scoreIds)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
break;
|
||||
|
||||
notification.Text = $"Upgrading scores to new scoring algorithm ({processedCount} of {scoreIds.Count})";
|
||||
notification.Progress = (float)processedCount / scoreIds.Count;
|
||||
|
||||
sleepIfRequired();
|
||||
|
||||
try
|
||||
{
|
||||
var score = scoreManager.Query(s => s.ID == id);
|
||||
long newTotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(score, beatmapManager);
|
||||
|
||||
// Can't use async overload because we're not on the update thread.
|
||||
// ReSharper disable once MethodHasAsyncOverload
|
||||
realmAccess.Write(r =>
|
||||
{
|
||||
ScoreInfo s = r.Find<ScoreInfo>(id);
|
||||
s.TotalScore = newTotalScore;
|
||||
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
|
||||
});
|
||||
|
||||
Logger.Log($"Converted total score for score {id}");
|
||||
++processedCount;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log($"Failed to convert total score for {id}: {e}");
|
||||
++failedCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (processedCount == scoreIds.Count)
|
||||
{
|
||||
notification.CompletionText = $"{processedCount} score(s) have been upgraded to the new scoring algorithm";
|
||||
notification.Progress = 1;
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
else
|
||||
{
|
||||
notification.Text = $"{processedCount} of {scoreIds.Count} score(s) have been upgraded to the new scoring algorithm.";
|
||||
|
||||
// We may have arrived here due to user cancellation or completion with failures.
|
||||
if (failedCount > 0)
|
||||
notification.Text += $" Check logs for issues with {failedCount} failed upgrades.";
|
||||
|
||||
notification.State = ProgressNotificationState.Cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
private void sleepIfRequired()
|
||||
{
|
||||
while (localUserPlayInfo?.IsPlaying.Value == true)
|
||||
{
|
||||
Logger.Log("Background processing sleeping due to active gameplay...");
|
||||
Thread.Sleep(TimeToSleepDuringGameplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,8 +78,9 @@ namespace osu.Game.Database
|
||||
/// 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.
|
||||
/// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations.
|
||||
/// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores.
|
||||
/// </summary>
|
||||
private const int schema_version = 30;
|
||||
private const int schema_version = 31;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@ -966,6 +967,25 @@ namespace osu.Game.Database
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 31:
|
||||
{
|
||||
foreach (var score in migration.NewRealm.All<ScoreInfo>())
|
||||
{
|
||||
if (score.IsLegacyScore && score.Ruleset.IsLegacyRuleset())
|
||||
{
|
||||
// Scores with this version will trigger the score upgrade process in BackgroundBeatmapProcessor.
|
||||
score.TotalScoreVersion = 30000002;
|
||||
|
||||
// Transfer known legacy scores to a permanent storage field for preservation.
|
||||
score.LegacyTotalScore = score.TotalScore;
|
||||
}
|
||||
else
|
||||
score.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||
|
@ -3,8 +3,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -185,6 +188,100 @@ namespace osu.Game.Database
|
||||
return (long)Math.Round((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
WorkingBeatmap beatmap = beatmaps.GetWorkingBeatmap(score.BeatmapInfo);
|
||||
Ruleset ruleset = score.Ruleset.CreateInstance();
|
||||
|
||||
if (ruleset is not ILegacyRuleset legacyRuleset)
|
||||
return score.TotalScore;
|
||||
|
||||
var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);
|
||||
|
||||
if (playableBeatmap.HitObjects.Count == 0)
|
||||
throw new InvalidOperationException("Beatmap contains no hit objects!");
|
||||
|
||||
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
|
||||
|
||||
sv1Simulator.Simulate(beatmap, playableBeatmap, score.Mods);
|
||||
|
||||
return ConvertFromLegacyTotalScore(score, new DifficultyAttributes
|
||||
{
|
||||
LegacyAccuracyScore = sv1Simulator.AccuracyScore,
|
||||
LegacyComboScore = sv1Simulator.ComboScore,
|
||||
LegacyBonusScoreRatio = sv1Simulator.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;
|
||||
|
||||
Debug.Assert(score.LegacyTotalScore != null);
|
||||
|
||||
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, ((long)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;
|
||||
|
@ -3,10 +3,10 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
@ -27,6 +27,9 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
protected const int ATTRIB_ID_FLASHLIGHT = 17;
|
||||
protected const int ATTRIB_ID_SLIDER_FACTOR = 19;
|
||||
protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21;
|
||||
protected const int ATTRIB_ID_LEGACY_ACCURACY_SCORE = 23;
|
||||
protected const int ATTRIB_ID_LEGACY_COMBO_SCORE = 25;
|
||||
protected const int ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO = 27;
|
||||
|
||||
/// <summary>
|
||||
/// The mods which were applied to the beatmap.
|
||||
@ -45,6 +48,22 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
[JsonProperty("max_combo", Order = -2)]
|
||||
public int MaxCombo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The accuracy portion of the legacy (ScoreV1) total score.
|
||||
/// </summary>
|
||||
public int LegacyAccuracyScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The combo-multiplied portion of the legacy (ScoreV1) total score.
|
||||
/// </summary>
|
||||
public int LegacyComboScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A ratio of <c>new_bonus_score / old_bonus_score</c> for converting the bonus score of legacy scores to the new scoring.
|
||||
/// This is made up of all judgements that would be <see cref="HitResult.SmallBonus"/> or <see cref="HitResult.LargeBonus"/>.
|
||||
/// </summary>
|
||||
public double LegacyBonusScoreRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates new <see cref="DifficultyAttributes"/>.
|
||||
/// </summary>
|
||||
@ -69,7 +88,13 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// <remarks>
|
||||
/// See: osu_difficulty_attribs table.
|
||||
/// </remarks>
|
||||
public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() => Enumerable.Empty<(int, object)>();
|
||||
public virtual IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
||||
yield return (ATTRIB_ID_LEGACY_ACCURACY_SCORE, LegacyAccuracyScore);
|
||||
yield return (ATTRIB_ID_LEGACY_COMBO_SCORE, LegacyComboScore);
|
||||
yield return (ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO, LegacyBonusScoreRatio);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads osu-web database attribute mappings into this <see cref="DifficultyAttributes"/> object.
|
||||
@ -78,6 +103,10 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// <param name="onlineInfo">The <see cref="IBeatmapOnlineInfo"/> where more information about the beatmap may be extracted from (such as AR/CS/OD/etc).</param>
|
||||
public virtual void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
{
|
||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
||||
LegacyAccuracyScore = (int)values[ATTRIB_ID_LEGACY_ACCURACY_SCORE];
|
||||
LegacyComboScore = (int)values[ATTRIB_ID_LEGACY_COMBO_SCORE];
|
||||
LegacyBonusScoreRatio = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
public abstract class DifficultyCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether legacy scoring values (ScoreV1) should be computed to populate the difficulty attributes
|
||||
/// <see cref="DifficultyAttributes.LegacyAccuracyScore"/>, <see cref="DifficultyAttributes.LegacyComboScore"/>,
|
||||
/// and <see cref="DifficultyAttributes.LegacyBonusScoreRatio"/>.
|
||||
/// </summary>
|
||||
public bool ComputeLegacyScoringValues;
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap for which difficulty will be calculated.
|
||||
/// </summary>
|
||||
|
@ -1,6 +1,8 @@
|
||||
// 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 osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
{
|
||||
public interface ILegacyRuleset
|
||||
@ -11,5 +13,7 @@ namespace osu.Game.Rulesets
|
||||
/// Identifies the server-side ID of a legacy ruleset.
|
||||
/// </summary>
|
||||
int LegacyID { get; }
|
||||
|
||||
ILegacyScoreSimulator CreateLegacyScoreSimulator();
|
||||
}
|
||||
}
|
||||
|
40
osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs
Normal file
40
osu.Game/Rulesets/Scoring/ILegacyScoreSimulator.cs
Normal file
@ -0,0 +1,40 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates attributes which are required to calculate old-style Score V1 scores.
|
||||
/// </summary>
|
||||
public interface ILegacyScoreSimulator
|
||||
{
|
||||
/// <summary>
|
||||
/// The accuracy portion of the legacy (ScoreV1) total score.
|
||||
/// </summary>
|
||||
int AccuracyScore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The combo-multiplied portion of the legacy (ScoreV1) total score.
|
||||
/// </summary>
|
||||
int ComboScore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A ratio of <c>new_bonus_score / old_bonus_score</c> for converting the bonus score of legacy scores to the new scoring.
|
||||
/// This is made up of all judgements that would be <see cref="HitResult.SmallBonus"/> or <see cref="HitResult.LargeBonus"/>.
|
||||
/// </summary>
|
||||
double BonusScoreRatio { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Performs the simulation, computing the maximum <see cref="AccuracyScore"/>, <see cref="ComboScore"/>,
|
||||
/// and <see cref="BonusScoreRatio"/> achievable for the given beatmap.
|
||||
/// </summary>
|
||||
/// <param name="workingBeatmap">The working beatmap.</param>
|
||||
/// <param name="playableBeatmap">A playable version of the beatmap for the ruleset.</param>
|
||||
/// <param name="mods">The applied mods.</param>
|
||||
void Simulate(IWorkingBeatmap workingBeatmap, IBeatmap playableBeatmap, IReadOnlyList<Mod> mods);
|
||||
}
|
||||
}
|
@ -28,10 +28,11 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>30000001: Appends <see cref="LegacyReplaySoloScoreInfo"/> to the end of scores.</description></item>
|
||||
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm.</description></item>
|
||||
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm. Legacy scores on this version are candidate to Score V1 -> V2 conversion.</description></item>
|
||||
/// <item><description>30000003: First version after converting legacy total score to standardised.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000002;
|
||||
public const int LATEST_VERSION = 30000003;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
@ -14,13 +14,7 @@ namespace osu.Game.Scoring.Legacy
|
||||
=> getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics);
|
||||
|
||||
public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode)
|
||||
{
|
||||
// Temporary to not scale stable scores that are already in the XX-millions with the classic scoring mode.
|
||||
if (scoreInfo.IsLegacyScore)
|
||||
return scoreInfo.TotalScore;
|
||||
|
||||
return getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
|
||||
}
|
||||
=> getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
|
||||
|
||||
private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary<HitResult, int> maximumStatistics)
|
||||
{
|
||||
|
@ -88,6 +88,11 @@ namespace osu.Game.Scoring
|
||||
// this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score.
|
||||
if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model))
|
||||
model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model);
|
||||
else if (model.IsLegacyScore)
|
||||
{
|
||||
model.LegacyTotalScore = model.TotalScore;
|
||||
model.TotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(model, beatmaps());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Utils;
|
||||
using Realms;
|
||||
@ -53,6 +54,26 @@ namespace osu.Game.Scoring
|
||||
|
||||
public long TotalScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of processing applied to calculate total score as stored in the database.
|
||||
/// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>,
|
||||
/// the total score has not yet been updated to reflect the current scoring values.
|
||||
///
|
||||
/// See <see cref="BackgroundBeatmapProcessor"/>'s conversion logic.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This may not match the version stored in the replay files.
|
||||
/// </remarks>
|
||||
internal int TotalScoreVersion { get; set; } = LegacyScoreEncoder.LATEST_VERSION;
|
||||
|
||||
/// <summary>
|
||||
/// Used to preserve the total score for legacy scores.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not populated if <see cref="IsLegacyScore"/> is <c>false</c>.
|
||||
/// </remarks>
|
||||
internal long? LegacyTotalScore { get; set; }
|
||||
|
||||
public int MaxCombo { get; set; }
|
||||
|
||||
public double Accuracy { get; set; }
|
||||
|
Loading…
Reference in New Issue
Block a user