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 hits = statistics[HitResult.Great] + statistics[HitResult.LargeTickHit] + statistics[HitResult.SmallTickHit];
double total = hits + statistics[HitResult.Miss] + statistics[HitResult.SmallTickMiss]; double total = hits + statistics[HitResult.Miss] + statistics[HitResult.SmallTickMiss];

View file

@ -36,12 +36,14 @@ namespace PerformanceCalculator.Simulate
public override Ruleset Ruleset => new ManiaRuleset(); 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). // 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) 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 perfectValue = mods.Any(m => m is ModClassic) ? 60 : 61;
int targetTotal = (int)Math.Round(accuracy * totalHits * 6);
// 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 // Start by assuming every non miss is a meh
// This is how much increase is needed by the rest // This is how much increase is needed by the rest
int remainingHits = totalHits - countMiss; 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) // Each perfect increases total by 50 (CL) or 51 (no CL) (perfect - meh = 50 or 51)
// There is no difference in accuracy between them, so just halve arbitrarily (favouring perfects for an odd number). int perfects = Math.Min(delta / (perfectValue - 10), remainingHits);
int greatsAndPerfects = Math.Min(delta / 5, remainingHits); delta -= perfects * (perfectValue - 10);
int greats = greatsAndPerfects / 2; remainingHits -= perfects;
int perfects = greatsAndPerfects - greats;
delta -= (greats + perfects) * 5;
remainingHits -= greats + perfects;
// Each good increases total by 3 (good-meh=3). // Each great increases total by 50 (great - meh = 50)
countGood = Math.Min(delta / 3, remainingHits); int greats = Math.Min(delta / 50, remainingHits);
delta -= countGood.Value * 3; 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; remainingHits -= countGood.Value;
// Each ok increases total by 1 (ok-meh=1). // Each ok increases total by 10 (ok - meh = 10)
int oks = delta; int oks = Math.Min(delta / 10, remainingHits);
remainingHits -= oks; remainingHits -= oks;
// Everything else is a meh, as initially assumed. // Everything else is a meh, as initially assumed
countMeh = remainingHits; countMeh = remainingHits;
return new Dictionary<HitResult, int> return new Dictionary<HitResult, int>
@ -96,5 +102,22 @@ namespace PerformanceCalculator.Simulate
{ HitResult.Miss, countMiss } { 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; 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 countGreat = statistics[HitResult.Great];
int countGood = statistics[HitResult.Ok]; int countGood = statistics[HitResult.Ok];

View file

@ -74,7 +74,7 @@ namespace PerformanceCalculator.Simulate
var statistics = GenerateHitResults(beatmap, mods); var statistics = GenerateHitResults(beatmap, mods);
var scoreInfo = new ScoreInfo(beatmap.BeatmapInfo, ruleset.RulesetInfo) 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), MaxCombo = Combo ?? (int)Math.Round(PercentCombo / 100 * beatmapMaxCombo),
Statistics = statistics, Statistics = statistics,
LegacyTotalScore = LegacyTotalScore, LegacyTotalScore = LegacyTotalScore,
@ -91,6 +91,6 @@ namespace PerformanceCalculator.Simulate
protected abstract Dictionary<HitResult, int> GenerateHitResults(IBeatmap beatmap, Mod[] mods); 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 countGreat = statistics[HitResult.Great];
int countGood = statistics[HitResult.Ok]; 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.Catch.Objects;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -61,14 +62,14 @@ namespace PerformanceCalculatorGUI
return (int)Math.Round(1000000 * scoreMultiplier); 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 return ruleset.OnlineID switch
{ {
0 => generateOsuHitResults(accuracy, beatmap, countMiss, countMeh, countGood, countLargeTickMisses, countSliderTailMisses), 0 => generateOsuHitResults(accuracy, beatmap, countMiss, countMeh, countGood, countLargeTickMisses, countSliderTailMisses),
1 => generateTaikoHitResults(accuracy, beatmap, countMiss, countGood), 1 => generateTaikoHitResults(accuracy, beatmap, countMiss, countGood),
2 => generateCatchHitResults(accuracy, beatmap, countMiss, countMeh, 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.") _ => 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 perfectValue = mods.Any(m => m is ModClassic) ? 60 : 61;
int targetTotal = (int)Math.Round(accuracy * totalResultCount * 6);
// 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 // Start by assuming every non miss is a meh
// This is how much increase is needed by greats and goods // This is how much increase is needed by the rest
int delta = targetTotal - (totalResultCount - countMiss); int remainingHits = totalHits - countMiss;
int delta = Math.Max(targetTotal - (10 * remainingHits), 0);
// Each great increases total by 5 (great-meh=5) // Each perfect increases total by 50 (CL) or 51 (no CL) (perfect - meh = 50 or 51)
int countGreat = delta / 5; int perfects = Math.Min(delta / (perfectValue - 10), remainingHits);
// Each good increases total by 1 (good-meh=1). Covers remaining difference. delta -= perfects * (perfectValue - 10);
int countGood = delta % 5; remainingHits -= perfects;
// Mehs are left over. Could be negative if impossible value of amountMiss chosen
int countMeh = totalResultCount - countGreat - countGood - countMiss; // 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> return new Dictionary<HitResult, int>
{ {
{ HitResult.Perfect, countGreat }, { HitResult.Perfect, perfects },
{ HitResult.Great, 0 }, { HitResult.Great, greats },
{ HitResult.Good, countGood }, { HitResult.Ok, oks },
{ HitResult.Ok, 0 }, { HitResult.Good, goods },
{ HitResult.Meh, countMeh }, { HitResult.Meh, mehs },
{ HitResult.Miss, countMiss } { 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 return ruleset.OnlineID switch
{ {
0 => getOsuAccuracy(beatmap, statistics), 0 => getOsuAccuracy(beatmap, statistics),
1 => getTaikoAccuracy(statistics), 1 => getTaikoAccuracy(statistics),
2 => getCatchAccuracy(statistics), 2 => getCatchAccuracy(statistics),
3 => getManiaAccuracy(statistics), 3 => getManiaAccuracy(statistics, mods),
_ => 0.0 _ => 0.0
}; };
} }
@ -314,7 +335,7 @@ namespace PerformanceCalculatorGUI
return hits / total; 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 countPerfect = statistics[HitResult.Perfect];
int countGreat = statistics[HitResult.Great]; int countGreat = statistics[HitResult.Great];
@ -322,11 +343,13 @@ namespace PerformanceCalculatorGUI
int countOk = statistics[HitResult.Ok]; int countOk = statistics[HitResult.Ok];
int countMeh = statistics[HitResult.Meh]; int countMeh = statistics[HitResult.Meh];
int countMiss = statistics[HitResult.Miss]; int countMiss = statistics[HitResult.Miss];
int total = countPerfect + countGreat + countGood + countOk + countMeh + countMiss;
return (double) int perfectWeight = mods.Any(m => m is ModClassic) ? 300 : 305;
((6 * (countPerfect + countGreat)) + (4 * countGood) + (2 * countOk) + countMeh) /
(6 * total); 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 // official rulesets can generate more precise hits from accuracy
if (appliedMods.Value.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value)) 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); null, null);
} }
else 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); 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) var ppAttributes = performanceCalculator?.Calculate(new ScoreInfo(beatmap.BeatmapInfo, ruleset.Value)