Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
76 changes: 56 additions & 20 deletions PerformanceCalculator/Simulate/OsuSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,32 +50,63 @@ 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 countSuccessfulHits;

// If there's no classic slider accuracy - we need to weight normal judgements accordingly.
// Normal judgements in this context are 300s, 100s, 50s and misses.
// Slider-related judgements are large tick hits/misses and slider tail hits/misses.
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)
{
countSuccessfulHits = 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;
countSuccessfulHits = 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
double relevantAccuracy = accuracy * totalResultCount / relevantResultCount;
double relevantAccuracy = accuracy * totalResultCount / countSuccessfulHits;

// Clamp accuracy to account for user trying to break the algorithm by inputting impossible values
relevantAccuracy = Math.Clamp(relevantAccuracy, 0, 1);
Expand All @@ -87,7 +118,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 * countSuccessfulHits * (1 - relevantAccuracy) / (5 * ratio50To100 + 4) * normalJudgementWeight;

// Get count50 according to c50 = c100 * ratio50to100
double count50Estimate = count100Estimate * ratio50To100;
Expand All @@ -102,23 +133,23 @@ private static Dictionary<HitResult, int> generateHitResults(IBeatmap beatmap, d
else if (relevantAccuracy >= 1.0 / 6)
{
// Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = 0
double count100Estimate = 6 * relevantResultCount * relevantAccuracy - relevantResultCount;
double count100Estimate = 6 * countSuccessfulHits * relevantAccuracy - countSuccessfulHits;

// We only had 100s and 50s in that scenario so rest of the hits are 50s
double count50Estimate = relevantResultCount - count100Estimate;
double count50Estimate = countSuccessfulHits - 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 +161,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 +177,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 +197,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
Loading
Loading