mirror of
https://github.com/ppy/osu.git
synced 2024-11-14 15:17:27 +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 int LegacyID => 2;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new CatchLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
|
||||||
|
|
||||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
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.
|
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
||||||
yield return (ATTRIB_ID_AIM, StarRating);
|
yield return (ATTRIB_ID_AIM, StarRating);
|
||||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
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];
|
StarRating = values[ATTRIB_ID_AIM];
|
||||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
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;
|
public override int Version => 20220701;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
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
|
// 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;
|
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,
|
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
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)),
|
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)
|
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())
|
foreach (var v in base.ToDatabaseAttributes())
|
||||||
yield return v;
|
yield return v;
|
||||||
|
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
}
|
}
|
||||||
@ -33,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
{
|
{
|
||||||
base.FromDatabaseAttributes(values, onlineInfo);
|
base.FromDatabaseAttributes(values, onlineInfo);
|
||||||
|
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
}
|
}
|
||||||
|
@ -31,9 +31,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
|
|
||||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||||
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
||||||
}
|
}
|
||||||
@ -46,15 +50,26 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
HitWindows hitWindows = new ManiaHitWindows();
|
HitWindows hitWindows = new ManiaHitWindows();
|
||||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
return new ManiaDifficultyAttributes
|
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
// 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.
|
// 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),
|
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)
|
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 int LegacyID => 3;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new ManiaLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
|
||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new ManiaRulesetConfigManager(settings, RulesetInfo);
|
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_SPEED, SpeedDifficulty);
|
||||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
|
|
||||||
if (ShouldSerializeFlashlightRating())
|
if (ShouldSerializeFlashlightRating())
|
||||||
@ -111,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||||
|
@ -26,9 +26,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
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
|
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 preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||||
double drainRate = beatmap.Difficulty.DrainRate;
|
double drainRate = beatmap.Difficulty.DrainRate;
|
||||||
@ -86,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||||
|
|
||||||
return new OsuDifficultyAttributes
|
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = starRating,
|
StarRating = starRating,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
@ -103,6 +108,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
SliderCount = sliderCount,
|
SliderCount = sliderCount,
|
||||||
SpinnerCount = spinnerCount,
|
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)
|
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 int LegacyID => 0;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new OsuLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
|
||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
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)
|
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;
|
DrawableDrumRollTick h;
|
||||||
DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType });
|
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())
|
foreach (var v in base.ToDatabaseAttributes())
|
||||||
yield return v;
|
yield return v;
|
||||||
|
|
||||||
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
|
|
||||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||||
}
|
}
|
||||||
@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
base.FromDatabaseAttributes(values, onlineInfo);
|
base.FromDatabaseAttributes(values, onlineInfo);
|
||||||
|
|
||||||
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
|
|
||||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||||
}
|
}
|
||||||
|
@ -25,9 +25,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
public override int Version => 20220902;
|
public override int Version => 20220902;
|
||||||
|
|
||||||
|
private readonly IWorkingBeatmap workingBeatmap;
|
||||||
|
|
||||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
|
workingBeatmap = beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
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 hitWindows = new TaikoHitWindows();
|
||||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||||
|
|
||||||
return new TaikoDifficultyAttributes
|
TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
|
||||||
{
|
{
|
||||||
StarRating = starRating,
|
StarRating = starRating,
|
||||||
Mods = mods,
|
Mods = mods,
|
||||||
@ -95,6 +98,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
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>
|
/// <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();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
AddNested(new DrumRollTick
|
AddNested(new DrumRollTick(this)
|
||||||
{
|
{
|
||||||
FirstTick = first,
|
FirstTick = first,
|
||||||
TickSpacing = tickSpacing,
|
TickSpacing = tickSpacing,
|
||||||
|
@ -9,6 +9,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
{
|
{
|
||||||
public class DrumRollTick : TaikoStrongableHitObject
|
public class DrumRollTick : TaikoStrongableHitObject
|
||||||
{
|
{
|
||||||
|
public readonly DrumRoll Parent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this is the first (initial) tick of the slider.
|
/// Whether this is the first (initial) tick of the slider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -25,6 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double HitWindow => TickSpacing / 2;
|
public double HitWindow => TickSpacing / 2;
|
||||||
|
|
||||||
|
public DrumRollTick(DrumRoll parent)
|
||||||
|
{
|
||||||
|
Parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
|
public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement();
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
@ -197,6 +197,8 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
public int LegacyID => 1;
|
public int LegacyID => 1;
|
||||||
|
|
||||||
|
public ILegacyScoreSimulator CreateLegacyScoreSimulator() => new TaikoLegacyScoreSimulator();
|
||||||
|
|
||||||
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
|
||||||
|
|
||||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
|
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
|
||||||
|
@ -14,8 +14,11 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Scoring.Legacy;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
namespace osu.Game
|
namespace osu.Game
|
||||||
@ -25,6 +28,9 @@ namespace osu.Game
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesetStore { get; set; } = null!;
|
private RulesetStore rulesetStore { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private ScoreManager scoreManager { get; set; } = null!;
|
private ScoreManager scoreManager { get; set; } = null!;
|
||||||
|
|
||||||
@ -40,19 +46,23 @@ namespace osu.Game
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
|
private ILocalUserPlayInfo? localUserPlayInfo { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private INotificationOverlay? notificationOverlay { get; set; }
|
||||||
|
|
||||||
protected virtual int TimeToSleepDuringGameplay => 30000;
|
protected virtual int TimeToSleepDuringGameplay => 30000;
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Task.Run(() =>
|
Task.Factory.StartNew(() =>
|
||||||
{
|
{
|
||||||
Logger.Log("Beginning background beatmap processing..");
|
Logger.Log("Beginning background beatmap processing..");
|
||||||
checkForOutdatedStarRatings();
|
checkForOutdatedStarRatings();
|
||||||
processBeatmapSetsWithMissingMetrics();
|
processBeatmapSetsWithMissingMetrics();
|
||||||
processScoresWithMissingStatistics();
|
processScoresWithMissingStatistics();
|
||||||
}).ContinueWith(t =>
|
convertLegacyTotalScoreToStandardised();
|
||||||
|
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
||||||
{
|
{
|
||||||
if (t.Exception?.InnerException is ObjectDisposedException)
|
if (t.Exception?.InnerException is ObjectDisposedException)
|
||||||
{
|
{
|
||||||
@ -121,11 +131,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
foreach (var id in beatmapSetIds)
|
foreach (var id in beatmapSetIds)
|
||||||
{
|
{
|
||||||
while (localUserPlayInfo?.IsPlaying.Value == true)
|
sleepIfRequired();
|
||||||
{
|
|
||||||
Logger.Log("Background processing sleeping due to active gameplay...");
|
|
||||||
Thread.Sleep(TimeToSleepDuringGameplay);
|
|
||||||
}
|
|
||||||
|
|
||||||
realmAccess.Run(r =>
|
realmAccess.Run(r =>
|
||||||
{
|
{
|
||||||
@ -166,11 +172,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
foreach (var id in scoreIds)
|
foreach (var id in scoreIds)
|
||||||
{
|
{
|
||||||
while (localUserPlayInfo?.IsPlaying.Value == true)
|
sleepIfRequired();
|
||||||
{
|
|
||||||
Logger.Log("Background processing sleeping due to active gameplay...");
|
|
||||||
Thread.Sleep(TimeToSleepDuringGameplay);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -187,11 +189,98 @@ namespace osu.Game
|
|||||||
|
|
||||||
Logger.Log($"Populated maximum statistics for score {id}");
|
Logger.Log($"Populated maximum statistics for score {id}");
|
||||||
}
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Log(@$"Failed to populate maximum statistics for {id}: {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.
|
/// 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.
|
/// 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.
|
/// 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>
|
/// </summary>
|
||||||
private const int schema_version = 30;
|
private const int schema_version = 31;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// 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;
|
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");
|
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
|
||||||
|
@ -3,8 +3,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
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);
|
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 class FakeHit : HitObject
|
||||||
{
|
{
|
||||||
private readonly Judgement judgement;
|
private readonly Judgement judgement;
|
||||||
|
@ -3,10 +3,10 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Difficulty
|
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_FLASHLIGHT = 17;
|
||||||
protected const int ATTRIB_ID_SLIDER_FACTOR = 19;
|
protected const int ATTRIB_ID_SLIDER_FACTOR = 19;
|
||||||
protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21;
|
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>
|
/// <summary>
|
||||||
/// The mods which were applied to the beatmap.
|
/// The mods which were applied to the beatmap.
|
||||||
@ -45,6 +48,22 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
[JsonProperty("max_combo", Order = -2)]
|
[JsonProperty("max_combo", Order = -2)]
|
||||||
public int MaxCombo { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Creates new <see cref="DifficultyAttributes"/>.
|
/// Creates new <see cref="DifficultyAttributes"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -69,7 +88,13 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// See: osu_difficulty_attribs table.
|
/// See: osu_difficulty_attribs table.
|
||||||
/// </remarks>
|
/// </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>
|
/// <summary>
|
||||||
/// Reads osu-web database attribute mappings into this <see cref="DifficultyAttributes"/> object.
|
/// 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>
|
/// <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)
|
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
|
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>
|
/// <summary>
|
||||||
/// The beatmap for which difficulty will be calculated.
|
/// The beatmap for which difficulty will be calculated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets
|
namespace osu.Game.Rulesets
|
||||||
{
|
{
|
||||||
public interface ILegacyRuleset
|
public interface ILegacyRuleset
|
||||||
@ -11,5 +13,7 @@ namespace osu.Game.Rulesets
|
|||||||
/// Identifies the server-side ID of a legacy ruleset.
|
/// Identifies the server-side ID of a legacy ruleset.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int LegacyID { get; }
|
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>
|
/// <remarks>
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description>30000001: Appends <see cref="LegacyReplaySoloScoreInfo"/> to the end of scores.</description></item>
|
/// <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>
|
/// </list>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public const int LATEST_VERSION = 30000002;
|
public const int LATEST_VERSION = 30000003;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
/// 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);
|
=> getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics);
|
||||||
|
|
||||||
public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode)
|
public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode)
|
||||||
{
|
=> getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary<HitResult, int> 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.
|
// this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score.
|
||||||
if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model))
|
if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model))
|
||||||
model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model);
|
model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model);
|
||||||
|
else if (model.IsLegacyScore)
|
||||||
|
{
|
||||||
|
model.LegacyTotalScore = model.TotalScore;
|
||||||
|
model.TotalScore = StandardisedScoreMigrationTools.ConvertFromLegacyTotalScore(model, beatmaps());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -15,6 +15,7 @@ using osu.Game.Online.API.Requests.Responses;
|
|||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Scoring.Legacy;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using osu.Game.Utils;
|
using osu.Game.Utils;
|
||||||
using Realms;
|
using Realms;
|
||||||
@ -53,6 +54,26 @@ namespace osu.Game.Scoring
|
|||||||
|
|
||||||
public long TotalScore { get; set; }
|
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 int MaxCombo { get; set; }
|
||||||
|
|
||||||
public double Accuracy { get; set; }
|
public double Accuracy { get; set; }
|
||||||
|
Loading…
Reference in New Issue
Block a user