1
0
Fork 0
mirror of https://github.com/ppy/osu-tools.git synced 2025-06-07 23:07:01 +09:00

Merge pull request #252 from MrHeliX/fix_simulate_mania

Fixes for mania simulate command
This commit is contained in:
StanR 2025-06-06 12:08:40 +03:00 committed by GitHub
commit 9272b2269f
Signed by: github
GPG key ID: B5690EEEBB952194
7 changed files with 99 additions and 53 deletions

View file

@ -68,7 +68,7 @@ namespace PerformanceCalculator.Simulate
};
}
protected override double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics)
protected override double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics, Mod[] mods)
{
double hits = statistics[HitResult.Great] + statistics[HitResult.LargeTickHit] + statistics[HitResult.SmallTickHit];
double total = hits + statistics[HitResult.Miss] + statistics[HitResult.SmallTickMiss];

View file

@ -36,12 +36,14 @@ namespace PerformanceCalculator.Simulate
public override Ruleset Ruleset => new ManiaRuleset();
protected override Dictionary<HitResult, int> GenerateHitResults(IBeatmap beatmap, Mod[] mods) => generateHitResults(beatmap, Accuracy / 100, Misses, Mehs, oks, Goods, greats);
protected override Dictionary<HitResult, int> GenerateHitResults(IBeatmap beatmap, Mod[] mods) => generateHitResults(beatmap, mods, Accuracy / 100, Misses, Mehs, oks, Goods, greats);
private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, double accuracy, int countMiss, int? countMeh, int? countOk, int? countGood, int? countGreat)
private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, Mod[] mods, double accuracy, int countMiss, int? countMeh, int? countOk, int? countGood, int? countGreat)
{
// One judgement per normal note. Two judgements per hold note (head + tail).
int totalHits = beatmap.HitObjects.Count + beatmap.HitObjects.Count(ho => ho is HoldNote);
int totalHits = beatmap.HitObjects.Count;
if (!mods.Any(m => m is ModClassic))
totalHits += beatmap.HitObjects.Count(ho => ho is HoldNote);
if (countMeh != null || countOk != null || countGood != null || countGreat != null)
{
@ -58,32 +60,36 @@ namespace PerformanceCalculator.Simulate
};
}
// Let Great=Perfect=6, Good=4, Ok=2, Meh=1, Miss=0. The total should be this.
int targetTotal = (int)Math.Round(accuracy * totalHits * 6);
int perfectValue = mods.Any(m => m is ModClassic) ? 60 : 61;
// Let Great = 60, Good = 40, Ok = 20, Meh = 10, Miss = 0, Perfect = 61 or 60 depending on CL. The total should be this.
int targetTotal = (int)Math.Round(accuracy * totalHits * perfectValue);
// Start by assuming every non miss is a meh
// This is how much increase is needed by the rest
int remainingHits = totalHits - countMiss;
int delta = targetTotal - remainingHits;
int delta = Math.Max(targetTotal - (10 * remainingHits), 0);
// Each great and perfect increases total by 5 (great-meh=5)
// There is no difference in accuracy between them, so just halve arbitrarily (favouring perfects for an odd number).
int greatsAndPerfects = Math.Min(delta / 5, remainingHits);
int greats = greatsAndPerfects / 2;
int perfects = greatsAndPerfects - greats;
delta -= (greats + perfects) * 5;
remainingHits -= greats + perfects;
// Each perfect increases total by 50 (CL) or 51 (no CL) (perfect - meh = 50 or 51)
int perfects = Math.Min(delta / (perfectValue - 10), remainingHits);
delta -= perfects * (perfectValue - 10);
remainingHits -= perfects;
// Each good increases total by 3 (good-meh=3).
countGood = Math.Min(delta / 3, remainingHits);
delta -= countGood.Value * 3;
// Each great increases total by 50 (great - meh = 50)
int greats = Math.Min(delta / 50, remainingHits);
delta -= greats * 50;
remainingHits -= greats;
// Each good increases total by 30 (good - meh = 30)
countGood = Math.Min(delta / 30, remainingHits);
delta -= countGood.Value * 30;
remainingHits -= countGood.Value;
// Each ok increases total by 1 (ok-meh=1).
int oks = delta;
// Each ok increases total by 10 (ok - meh = 10)
int oks = Math.Min(delta / 10, remainingHits);
remainingHits -= oks;
// Everything else is a meh, as initially assumed.
// Everything else is a meh, as initially assumed
countMeh = remainingHits;
return new Dictionary<HitResult, int>
@ -96,5 +102,22 @@ namespace PerformanceCalculator.Simulate
{ HitResult.Miss, countMiss }
};
}
protected override double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics, Mod[] mods)
{
int countPerfect = statistics[HitResult.Perfect];
int countGreat = statistics[HitResult.Great];
int countGood = statistics[HitResult.Good];
int countOk = statistics[HitResult.Ok];
int countMeh = statistics[HitResult.Meh];
int countMiss = statistics[HitResult.Miss];
int perfectWeight = mods.Any(m => m is ModClassic) ? 300 : 305;
double total = (perfectWeight * countPerfect) + (300 * countGreat) + (200 * countGood) + (100 * countOk) + (50 * countMeh);
double max = perfectWeight * (countPerfect + countGreat + countGood + countOk + countMeh + countMiss);
return total / max;
}
}
}

View file

@ -151,7 +151,7 @@ namespace PerformanceCalculator.Simulate
return result;
}
protected override double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics)
protected override double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics, Mod[] mods)
{
int countGreat = statistics[HitResult.Great];
int countGood = statistics[HitResult.Ok];

View file

@ -74,7 +74,7 @@ namespace PerformanceCalculator.Simulate
var statistics = GenerateHitResults(beatmap, mods);
var scoreInfo = new ScoreInfo(beatmap.BeatmapInfo, ruleset.RulesetInfo)
{
Accuracy = GetAccuracy(beatmap, statistics),
Accuracy = GetAccuracy(beatmap, statistics, mods),
MaxCombo = Combo ?? (int)Math.Round(PercentCombo / 100 * beatmapMaxCombo),
Statistics = statistics,
LegacyTotalScore = LegacyTotalScore,
@ -91,6 +91,6 @@ namespace PerformanceCalculator.Simulate
protected abstract Dictionary<HitResult, int> GenerateHitResults(IBeatmap beatmap, Mod[] mods);
protected virtual double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics) => 0;
protected virtual double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics, Mod[] mods) => 0;
}
}

View file

@ -60,7 +60,7 @@ namespace PerformanceCalculator.Simulate
};
}
protected override double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics)
protected override double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics, Mod[] mods)
{
int countGreat = statistics[HitResult.Great];
int countGood = statistics[HitResult.Ok];

View file

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
@ -61,14 +62,14 @@ namespace PerformanceCalculatorGUI
return (int)Math.Round(1000000 * scoreMultiplier);
}
public static Dictionary<HitResult, int> GenerateHitResultsForRuleset(RulesetInfo ruleset, double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses)
public static Dictionary<HitResult, int> GenerateHitResultsForRuleset(RulesetInfo ruleset, double accuracy, IBeatmap beatmap, Mod[] mods, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses)
{
return ruleset.OnlineID switch
{
0 => generateOsuHitResults(accuracy, beatmap, countMiss, countMeh, countGood, countLargeTickMisses, countSliderTailMisses),
1 => generateTaikoHitResults(accuracy, beatmap, countMiss, countGood),
2 => generateCatchHitResults(accuracy, beatmap, countMiss, countMeh, countGood),
3 => generateManiaHitResults(accuracy, beatmap, countMiss),
3 => generateManiaHitResults(accuracy, beatmap, mods, countMiss),
_ => throw new ArgumentException("Invalid ruleset ID provided.")
};
}
@ -225,43 +226,63 @@ namespace PerformanceCalculatorGUI
};
}
private static Dictionary<HitResult, int> generateManiaHitResults(double accuracy, IBeatmap beatmap, int countMiss)
private static Dictionary<HitResult, int> generateManiaHitResults(double accuracy, IBeatmap beatmap, Mod[] mods, int countMiss)
{
int totalResultCount = beatmap.HitObjects.Count;
int totalHits = beatmap.HitObjects.Count;
if (!mods.Any(m => m is ModClassic))
totalHits += beatmap.HitObjects.Count(ho => ho is HoldNote);
// Let Great=6, Good=2, Meh=1, Miss=0. The total should be this.
int targetTotal = (int)Math.Round(accuracy * totalResultCount * 6);
int perfectValue = mods.Any(m => m is ModClassic) ? 60 : 61;
// Let Great = 60, Good = 40, Ok = 20, Meh = 10, Miss = 0, Perfect = 61 or 60 depending on CL. The total should be this.
int targetTotal = (int)Math.Round(accuracy * totalHits * perfectValue);
// Start by assuming every non miss is a meh
// This is how much increase is needed by greats and goods
int delta = targetTotal - (totalResultCount - countMiss);
// This is how much increase is needed by the rest
int remainingHits = totalHits - countMiss;
int delta = Math.Max(targetTotal - (10 * remainingHits), 0);
// Each great increases total by 5 (great-meh=5)
int countGreat = delta / 5;
// Each good increases total by 1 (good-meh=1). Covers remaining difference.
int countGood = delta % 5;
// Mehs are left over. Could be negative if impossible value of amountMiss chosen
int countMeh = totalResultCount - countGreat - countGood - countMiss;
// Each perfect increases total by 50 (CL) or 51 (no CL) (perfect - meh = 50 or 51)
int perfects = Math.Min(delta / (perfectValue - 10), remainingHits);
delta -= perfects * (perfectValue - 10);
remainingHits -= perfects;
// Each great increases total by 50 (great - meh = 50)
int greats = Math.Min(delta / 50, remainingHits);
delta -= greats * 50;
remainingHits -= greats;
// Each good increases total by 30 (good - meh = 30)
int goods = Math.Min(delta / 30, remainingHits);
delta -= goods * 30;
remainingHits -= goods;
// Each ok increases total by 10 (ok - meh = 10)
int oks = Math.Min(delta / 10, remainingHits);
remainingHits -= oks;
// Everything else is a meh, as initially assumed
int mehs = remainingHits;
return new Dictionary<HitResult, int>
{
{ HitResult.Perfect, countGreat },
{ HitResult.Great, 0 },
{ HitResult.Good, countGood },
{ HitResult.Ok, 0 },
{ HitResult.Meh, countMeh },
{ HitResult.Perfect, perfects },
{ HitResult.Great, greats },
{ HitResult.Ok, oks },
{ HitResult.Good, goods },
{ HitResult.Meh, mehs },
{ HitResult.Miss, countMiss }
};
}
public static double GetAccuracyForRuleset(RulesetInfo ruleset, IBeatmap beatmap, Dictionary<HitResult, int> statistics)
public static double GetAccuracyForRuleset(RulesetInfo ruleset, IBeatmap beatmap, Dictionary<HitResult, int> statistics, Mod[] mods)
{
return ruleset.OnlineID switch
{
0 => getOsuAccuracy(beatmap, statistics),
1 => getTaikoAccuracy(statistics),
2 => getCatchAccuracy(statistics),
3 => getManiaAccuracy(statistics),
3 => getManiaAccuracy(statistics, mods),
_ => 0.0
};
}
@ -314,7 +335,7 @@ namespace PerformanceCalculatorGUI
return hits / total;
}
private static double getManiaAccuracy(Dictionary<HitResult, int> statistics)
private static double getManiaAccuracy(Dictionary<HitResult, int> statistics, Mod[] mods)
{
int countPerfect = statistics[HitResult.Perfect];
int countGreat = statistics[HitResult.Great];
@ -322,11 +343,13 @@ namespace PerformanceCalculatorGUI
int countOk = statistics[HitResult.Ok];
int countMeh = statistics[HitResult.Meh];
int countMiss = statistics[HitResult.Miss];
int total = countPerfect + countGreat + countGood + countOk + countMeh + countMiss;
return (double)
((6 * (countPerfect + countGreat)) + (4 * countGood) + (2 * countOk) + countMeh) /
(6 * total);
int perfectWeight = mods.Any(m => m is ModClassic) ? 300 : 305;
double total = (perfectWeight * countPerfect) + (300 * countGreat) + (200 * countGood) + (100 * countOk) + (50 * countMeh);
double max = perfectWeight * (countPerfect + countGreat + countGood + countOk + countMeh + countMiss);
return total / max;
}
}
}

View file

@ -714,16 +714,16 @@ namespace PerformanceCalculatorGUI.Screens
// official rulesets can generate more precise hits from accuracy
if (appliedMods.Value.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value))
{
statistics = RulesetHelper.GenerateHitResultsForRuleset(ruleset.Value, accuracyTextBox.Value.Value / 100.0, beatmap, missesTextBox.Value.Value, countMeh, countGood,
statistics = RulesetHelper.GenerateHitResultsForRuleset(ruleset.Value, accuracyTextBox.Value.Value / 100.0, beatmap, appliedMods.Value.ToArray(), missesTextBox.Value.Value, countMeh, countGood,
null, null);
}
else
{
statistics = RulesetHelper.GenerateHitResultsForRuleset(ruleset.Value, accuracyTextBox.Value.Value / 100.0, beatmap, missesTextBox.Value.Value, countMeh, countGood,
statistics = RulesetHelper.GenerateHitResultsForRuleset(ruleset.Value, accuracyTextBox.Value.Value / 100.0, beatmap, appliedMods.Value.ToArray(), missesTextBox.Value.Value, countMeh, countGood,
largeTickMissesTextBox.Value.Value, sliderTailMissesTextBox.Value.Value);
}
accuracy = RulesetHelper.GetAccuracyForRuleset(ruleset.Value, beatmap, statistics);
accuracy = RulesetHelper.GetAccuracyForRuleset(ruleset.Value, beatmap, statistics, appliedMods.Value.ToArray());
}
var ppAttributes = performanceCalculator?.Calculate(new ScoreInfo(beatmap.BeatmapInfo, ruleset.Value)