Skip to content

Fixes for mania simulate command #252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 6, 2025
Merged
2 changes: 1 addition & 1 deletion PerformanceCalculator/Simulate/CatchSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, d
};
}

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];
Expand Down
65 changes: 44 additions & 21 deletions PerformanceCalculator/Simulate/ManiaSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ public class ManiaSimulateCommand : SimulateCommand

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)
{
Expand All @@ -58,32 +60,36 @@ private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, d
};
}

// 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;

// 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 good increases total by 3 (good-meh=3).
countGood = Math.Min(delta / 3, remainingHits);
delta -= countGood.Value * 3;
int delta = Math.Max(targetTotal - (10 * remainingHits), 0);

// 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)
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>
Expand All @@ -96,5 +102,22 @@ private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, d
{ 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;
}
}
}
2 changes: 1 addition & 1 deletion PerformanceCalculator/Simulate/OsuSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, d
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];
Expand Down
4 changes: 2 additions & 2 deletions PerformanceCalculator/Simulate/SimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public override void Execute()
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,
Expand All @@ -91,6 +91,6 @@ public override void Execute()

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;
}
}
2 changes: 1 addition & 1 deletion PerformanceCalculator/Simulate/TaikoSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static Dictionary<HitResult, int> generateHitResults(double accuracy, IB
};
}

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];
Expand Down
75 changes: 49 additions & 26 deletions PerformanceCalculatorGUI/RulesetHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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;
Expand Down Expand Up @@ -61,14 +62,14 @@ public static int AdjustManiaScore(int score, IReadOnlyList<Mod> mods)
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.")
};
}
Expand Down Expand Up @@ -225,43 +226,63 @@ private static Dictionary<HitResult, int> generateCatchHitResults(double accurac
};
}

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);

int perfectValue = mods.Any(m => m is ModClassic) ? 60 : 61;

// Let Great=6, Good=2, Meh=1, Miss=0. The total should be this.
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
// 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 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 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 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
};
}
Expand Down Expand Up @@ -314,19 +335,21 @@ private static double getCatchAccuracy(Dictionary<HitResult, int> statistics)
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];
int countGood = statistics[HitResult.Good];
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;
}
}
}
6 changes: 3 additions & 3 deletions PerformanceCalculatorGUI/Screens/SimulateScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -714,16 +714,16 @@ private void calculatePerformance()
// 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)
Expand Down
Loading