Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 51 additions & 17 deletions PerformanceCalculator/Simulate/OsuSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,57 @@ protected override Dictionary<HitResult, int> GenerateHitResults(IBeatmap beatma
// Use lazer info only if score has sliderhead accuracy
if (mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value))
{
return generateHitResults(beatmap, Accuracy / 100, Misses, Mehs, Goods, null, null);
return generateHitResults(beatmap, Accuracy / 100, mods, Misses, Mehs, Goods, null, null);
}
else
{
return generateHitResults(beatmap, Accuracy / 100, Misses, Mehs, Goods, largeTickMisses, sliderTailMisses);
return generateHitResults(beatmap, Accuracy / 100, mods, Misses, Mehs, Goods, largeTickMisses, sliderTailMisses);
}
}

private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, double accuracy, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses)
private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, double accuracy, Mod[] mods, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses)
{
bool usingClassicSliderAccuracy = mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);

int countGreat;

int totalResultCount = beatmap.HitObjects.Count;

int countLargeTicks = beatmap.HitObjects.Sum(obj => obj.NestedHitObjects.Count(x => x is SliderTick or SliderRepeat));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

count... is used for hitresults here, so maybe total... instead? preferably for the other usages in this file that already exist as well, to reduce the confusion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totalLargeTicks doesn't sounds good to me
totalResultCount has "count" in it, and "totalResult" is a descriptor of what kind of count it is
maybe rename to largeTickCount would be better?

int countSmallTicks = beatmap.HitObjects.Count(x => x is Slider);

// Sliderheads are large ticks too if slideracc is disabled
if (usingClassicSliderAccuracy)
countLargeTicks += countSmallTicks;

countLargeTickMisses = Math.Min(countLargeTickMisses ?? 0, countLargeTicks);
countSliderTailMisses = Math.Min(countSliderTailMisses ?? 0, countSmallTicks);

if (countMeh != null || countGood != null)
{
countGreat = totalResultCount - (countGood ?? 0) - (countMeh ?? 0) - countMiss;
}
else
{
// Total result count excluding countMiss
int relevantResultCount = totalResultCount - countMiss;
// Relevant result count without misses (normal misses and slider-related misses)
// We need to exclude them from judgement count so total value will be equal to desired after misses are accounted for
double relevantResultCount;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's not "true" count successful hits because in non-CL case it's also weighted
Like it's just a number used for calculations, it doesn't have some objective meaning

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine even if its weighted, im mostly concerned with the naming being self-explanatory enough for people to grasp the idea behind all this without too much strain - right now if i see relevantResultCount somewhere down the line it requires scrolling up to read the comment above it to remember what it actually is


// If there's no classic slider accuracy - we need to weight circle judgements accordingly
double normalJudgementWeight = 1.0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

circleJudgementsWeight?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also appreciate a comment as to why this exist and why we need to use it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example let's assume that map has enough sliders so 10% of the score belongs to sliderends and sliderpoints. In this case we need to compensate for the fact that "circles" (includes sliderheads and spinners) only hold 90% of the influence they would've had in the CL scenario where they're the only type of judgments.

circleJudgementWeight is technically inaccurate (I probably need to fix comment above). This refers to 300s/100s/50s/misses. When "not normal" judgements are slider end hits and big tick hits.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nonNestedJudgementsWeight then maybe? not really sure what'd be the best naming for this to make it self-explanatory. maybe an explanation comment would be enough, not sure
@tsunyoku any ideas?


if (usingClassicSliderAccuracy)
{
relevantResultCount = totalResultCount - countMiss;
}
else
{
double maxSliderPortion = countSmallTicks * 0.5 + countLargeTicks * 0.1;
normalJudgementWeight = (totalResultCount + maxSliderPortion) / totalResultCount;

double missedSliderPortion = (double)countSliderTailMisses * 0.5 + (double)countLargeTickMisses * 0.1;
relevantResultCount = totalResultCount - (countMiss + missedSliderPortion) / normalJudgementWeight;
}

// Accuracy excluding countMiss. We need that because we're trying to achieve target accuracy without touching countMiss
// So it's better to pretened that there were 0 misses in the 1st place
Expand All @@ -87,7 +116,7 @@ private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, d
double ratio50To100 = Math.Pow(1 - (relevantAccuracy - 0.25) / 0.75, 2);

// Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c50 = c100 * ratio50to100
double count100Estimate = 6 * relevantResultCount * (1 - relevantAccuracy) / (5 * ratio50To100 + 4);
double count100Estimate = 6 * relevantResultCount * (1 - relevantAccuracy) / (5 * ratio50To100 + 4) * normalJudgementWeight;

// Get count50 according to c50 = c100 * ratio50to100
double count50Estimate = count100Estimate * ratio50To100;
Expand All @@ -108,17 +137,17 @@ private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, d
double count50Estimate = relevantResultCount - count100Estimate;

// Round it to get int number of 100s
countGood = (int?)Math.Round(count100Estimate);
countGood = (int?)Math.Round(count100Estimate * normalJudgementWeight);

// Get number of 50s as difference between total mistimed hits and count100
countMeh = (int?)(Math.Round(count100Estimate + count50Estimate) - countGood);
countMeh = (int?)(Math.Round((count100Estimate + count50Estimate) * normalJudgementWeight) - countGood);
}
// If accuracy is less than 16.67% - it means that we have only 50s or misses
// Assuming that we removed misses in the 1st place - that means that we need to add additional misses to achieve target accuracy
else
{
// Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = c100 = 0
double count50Estimate = 6 * relevantResultCount * relevantAccuracy;
double count50Estimate = 6 * (totalResultCount - countMiss) * relevantAccuracy;

// We have 0 100s, because we can't start adding 100s again after reaching "only 50s" point
countGood = 0;
Expand All @@ -130,6 +159,10 @@ private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, d
countMiss = (int)(totalResultCount - countMeh);
}

// Clamp goods if total amount is bigger than possible
countGood -= Math.Clamp((int)(countGood + countMeh + countMiss - totalResultCount), 0, (int)countGood);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be = instead of -=?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is correct. Thanks to increased weight (we add more 100s than in CL case) we can add more 100s than possible. This line is removing those extra impossible 100s.
totalResultCount is the maximum amount of judgements we can have. And countGood + countMeh + countMiss is total amount of judgements we have generated. If it's greater than first - remove the extra judgements.
This is only relevant for very low acc scores.

countMeh -= Math.Clamp((int)(countGood + countMeh + countMiss - totalResultCount), 0, (int)countMeh);

// Rest of the hits are 300s
countGreat = (int)(totalResultCount - countGood - countMeh - countMiss);
}
Expand All @@ -142,17 +175,18 @@ private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, d
{ HitResult.Miss, countMiss }
};

if (countLargeTickMisses != null)
result[HitResult.LargeTickMiss] = countLargeTickMisses.Value;

if (countSliderTailMisses != null)
result[HitResult.SliderTailHit] = beatmap.HitObjects.Count(x => x is Slider) - countSliderTailMisses.Value;
result[HitResult.LargeTickHit] = countLargeTicks - (int)countLargeTickMisses;
result[HitResult.LargeTickMiss] = (int)countLargeTickMisses;
result[usingClassicSliderAccuracy ? HitResult.SmallTickHit : HitResult.SliderTailHit] = countSmallTicks - (int)countSliderTailMisses;
if (usingClassicSliderAccuracy) result[HitResult.SmallTickMiss] = (int)countSliderTailMisses;

return result;
}

protected override double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics, Mod[] mods)
{
bool usingClassicSliderAccuracy = mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);

int countGreat = statistics[HitResult.Great];
int countGood = statistics[HitResult.Ok];
int countMeh = statistics[HitResult.Meh];
Expand All @@ -161,18 +195,18 @@ protected override double GetAccuracy(IBeatmap beatmap, Dictionary<HitResult, in
double total = 6 * countGreat + 2 * countGood + countMeh;
double max = 6 * (countGreat + countGood + countMeh + countMiss);

if (statistics.TryGetValue(HitResult.SliderTailHit, out int countSliderTailHit))
if (!usingClassicSliderAccuracy && statistics.TryGetValue(HitResult.SliderTailHit, out int countSliderTailHit))
{
int countSliders = beatmap.HitObjects.Count(x => x is Slider);

total += 3 * countSliderTailHit;
max += 3 * countSliders;
}

if (statistics.TryGetValue(HitResult.LargeTickMiss, out int countLargeTickMiss))
if (!usingClassicSliderAccuracy && statistics.TryGetValue(HitResult.LargeTickMiss, out int countLargeTicksMiss))
{
int countLargeTicks = beatmap.HitObjects.Sum(obj => obj.NestedHitObjects.Count(x => x is SliderTick or SliderRepeat));
int countLargeTickHit = countLargeTicks - countLargeTickMiss;
int countLargeTickHit = countLargeTicks - countLargeTicksMiss;

total += 0.6 * countLargeTickHit;
max += 0.6 * countLargeTicks;
Expand Down
69 changes: 52 additions & 17 deletions PerformanceCalculatorGUI/RulesetHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko;
Expand Down Expand Up @@ -66,28 +67,57 @@ public static Dictionary<HitResult, int> GenerateHitResultsForRuleset(RulesetInf
{
return ruleset.OnlineID switch
{
0 => generateOsuHitResults(accuracy, beatmap, countMiss, countMeh, countGood, countLargeTickMisses, countSliderTailMisses),
0 => generateOsuHitResults(accuracy, beatmap, mods, countMiss, countMeh, countGood, countLargeTickMisses, countSliderTailMisses),
1 => generateTaikoHitResults(accuracy, beatmap, countMiss, countGood),
2 => generateCatchHitResults(accuracy, beatmap, countMiss, countMeh, countGood),
3 => generateManiaHitResults(accuracy, beatmap, mods, countMiss),
_ => throw new ArgumentException("Invalid ruleset ID provided.")
};
}

private static Dictionary<HitResult, int> generateOsuHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses)
private static Dictionary<HitResult, int> generateOsuHitResults(double accuracy, IBeatmap beatmap, Mod[] mods, int countMiss, int? countMeh, int? countGood, int? countLargeTickMisses, int? countSliderTailMisses)
{
bool usingClassicSliderAccuracy = mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);

int countGreat;

int totalResultCount = beatmap.HitObjects.Count;

int countLargeTicks = beatmap.HitObjects.Sum(obj => obj.NestedHitObjects.Count(x => x is SliderTick or SliderRepeat));
int countSmallTicks = beatmap.HitObjects.Count(x => x is Slider);

// Sliderheads are large ticks too if slideracc is disabled
if (usingClassicSliderAccuracy)
countLargeTicks += countSmallTicks;

countLargeTickMisses = Math.Min(countLargeTickMisses ?? 0, countLargeTicks);
countSliderTailMisses = Math.Min(countSliderTailMisses ?? 0, countSmallTicks);

if (countMeh != null || countGood != null)
{
countGreat = totalResultCount - (countGood ?? 0) - (countMeh ?? 0) - countMiss;
}
else
{
// Total result count excluding countMiss
int relevantResultCount = totalResultCount - countMiss;
// Relevant result count without misses (normal misses and slider-related misses)
// We need to exclude them from judgement count so total value will be equal to desired after misses are accounted for
double relevantResultCount;

// If there's no classic slider accuracy - we need to weight circle judgements accordingly
double normalJudgementWeight = 1.0;

if (usingClassicSliderAccuracy)
{
relevantResultCount = totalResultCount - countMiss;
}
else
{
double maxSliderPortion = countSmallTicks * 0.5 + countLargeTicks * 0.1;
normalJudgementWeight = (totalResultCount + maxSliderPortion) / totalResultCount;

double missedSliderPortion = (double)countSliderTailMisses * 0.5 + (double)countLargeTickMisses * 0.1;
relevantResultCount = totalResultCount - (countMiss + missedSliderPortion) / normalJudgementWeight;
}

// Accuracy excluding countMiss. We need that because we're trying to achieve target accuracy without touching countMiss
// So it's better to pretened that there were 0 misses in the 1st place
Expand All @@ -103,7 +133,7 @@ private static Dictionary<HitResult, int> generateOsuHitResults(double accuracy,
double ratio50To100 = Math.Pow(1 - (relevantAccuracy - 0.25) / 0.75, 2);

// Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c50 = c100 * ratio50to100
double count100Estimate = 6 * relevantResultCount * (1 - relevantAccuracy) / (5 * ratio50To100 + 4);
double count100Estimate = 6 * relevantResultCount * (1 - relevantAccuracy) / (5 * ratio50To100 + 4) * normalJudgementWeight;

// Get count50 according to c50 = c100 * ratio50to100
double count50Estimate = count100Estimate * ratio50To100;
Expand All @@ -124,17 +154,17 @@ private static Dictionary<HitResult, int> generateOsuHitResults(double accuracy,
double count50Estimate = relevantResultCount - count100Estimate;

// Round it to get int number of 100s
countGood = (int?)Math.Round(count100Estimate);
countGood = (int?)Math.Round(count100Estimate * normalJudgementWeight);

// Get number of 50s as difference between total mistimed hits and count100
countMeh = (int?)(Math.Round(count100Estimate + count50Estimate) - countGood);
countMeh = (int?)(Math.Round((count100Estimate + count50Estimate) * normalJudgementWeight) - countGood);
}
// If accuracy is less than 16.67% - it means that we have only 50s or misses
// Assuming that we removed misses in the 1st place - that means that we need to add additional misses to achieve target accuracy
else
{
// Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = c100 = 0
double count50Estimate = 6 * relevantResultCount * relevantAccuracy;
double count50Estimate = 6 * (totalResultCount - countMiss) * relevantAccuracy;

// We have 0 100s, because we can't start adding 100s again after reaching "only 50s" point
countGood = 0;
Expand All @@ -146,6 +176,10 @@ private static Dictionary<HitResult, int> generateOsuHitResults(double accuracy,
countMiss = (int)(totalResultCount - countMeh);
}

// Clamp goods if total amount is bigger than possible
countGood -= Math.Clamp((int)(countGood + countMeh + countMiss - totalResultCount), 0, (int)countGood);
countMeh -= Math.Clamp((int)(countGood + countMeh + countMiss - totalResultCount), 0, (int)countMeh);

// Rest of the hits are 300s
countGreat = (int)(totalResultCount - countGood - countMeh - countMiss);
}
Expand All @@ -158,11 +192,10 @@ private static Dictionary<HitResult, int> generateOsuHitResults(double accuracy,
{ HitResult.Miss, countMiss }
};

if (countLargeTickMisses != null)
result[HitResult.LargeTickMiss] = countLargeTickMisses.Value;

if (countSliderTailMisses != null)
result[HitResult.SliderTailHit] = beatmap.HitObjects.Count(x => x is Slider) - countSliderTailMisses.Value;
result[HitResult.LargeTickHit] = countLargeTicks - (int)countLargeTickMisses;
result[HitResult.LargeTickMiss] = (int)countLargeTickMisses;
result[usingClassicSliderAccuracy ? HitResult.SmallTickHit : HitResult.SliderTailHit] = countSmallTicks - (int)countSliderTailMisses;
if (usingClassicSliderAccuracy) result[HitResult.SmallTickMiss] = (int)countSliderTailMisses;

return result;
}
Expand Down Expand Up @@ -279,16 +312,18 @@ public static double GetAccuracyForRuleset(RulesetInfo ruleset, IBeatmap beatmap
{
return ruleset.OnlineID switch
{
0 => getOsuAccuracy(beatmap, statistics),
0 => getOsuAccuracy(beatmap, statistics, mods),
1 => getTaikoAccuracy(statistics),
2 => getCatchAccuracy(statistics),
3 => getManiaAccuracy(statistics, mods),
_ => 0.0
};
}

private static double getOsuAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics)
private static double getOsuAccuracy(IBeatmap beatmap, Dictionary<HitResult, int> statistics, Mod[] mods)
{
bool usingClassicSliderAccuracy = mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);

int countGreat = statistics[HitResult.Great];
int countGood = statistics[HitResult.Ok];
int countMeh = statistics[HitResult.Meh];
Expand All @@ -297,15 +332,15 @@ private static double getOsuAccuracy(IBeatmap beatmap, Dictionary<HitResult, int
double total = 6 * countGreat + 2 * countGood + countMeh;
double max = 6 * (countGreat + countGood + countMeh + countMiss);

if (statistics.TryGetValue(HitResult.SliderTailHit, out int countSliderTailHit))
if (!usingClassicSliderAccuracy && statistics.TryGetValue(HitResult.SliderTailHit, out int countSliderTailHit))
{
int countSliders = beatmap.HitObjects.Count(x => x is Slider);

total += 3 * countSliderTailHit;
max += 3 * countSliders;
}

if (statistics.TryGetValue(HitResult.LargeTickMiss, out int countLargeTicksMiss))
if (!usingClassicSliderAccuracy && statistics.TryGetValue(HitResult.LargeTickMiss, out int countLargeTicksMiss))
{
int countLargeTicks = beatmap.HitObjects.Sum(obj => obj.NestedHitObjects.Count(x => x is SliderTick or SliderRepeat));
int countLargeTickHit = countLargeTicks - countLargeTicksMiss;
Expand Down
Loading