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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static class FlowAimEvaluator
/// <summary>
/// Evaluates difficulty of "flow aim" - aiming pattern where player doesn't stop their cursor on every object and instead "flows" through them.
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance)
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliderTravelDistance, bool aimCheese)
{
var osuCurrObj = (OsuDifficultyHitObject)current;
var osuLastObj = (OsuDifficultyHitObject)current.Previous();
Expand Down Expand Up @@ -110,8 +110,11 @@ public static double EvaluateDifficultyOf(DifficultyHitObject current, bool with
// Final velocity is being raised to a power because flow difficulty scales harder with both high distance and time, and we want to account for that
flowDifficulty = DiffUtils.Pow(flowDifficulty, 1.45);

// Reduce difficulty for low spacing since spacing below radius is always to be flowed
return flowDifficulty * DiffUtils.Smootherstep(currDistance, 0, OsuDifficultyHitObject.NORMALISED_RADIUS);
// Check how much of the difficulty relies on small distances that can be cheesed by playing them improperly
if (aimCheese)
flowDifficulty *= DiffUtils.Smootherstep(currDistance, 0, OsuDifficultyHitObject.NORMALISED_DIAMETER);

return flowDifficulty;
}

private static double calculateOverlapFactor(OsuDifficultyHitObject first, OsuDifficultyHitObject second)
Expand Down
8 changes: 8 additions & 0 deletions osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ public class OsuDifficultyAttributes : DifficultyAttributes
[JsonProperty("slider_factor")]
public double SliderFactor { get; set; }

/// <summary>
/// Describes how much of <see cref="AimDifficulty"/> is contributed to by cheesable patterns..
/// A value closer to 1.0 indicates most of <see cref="AimDifficulty"/> is contributed by normal patterns.
/// A value closer to 0.0 indicates most of <see cref="AimDifficulty"/> is contributed by cheesable patterns.
/// </summary>
[JsonProperty("cheese_factor")]
public double CheeseFactor { get; set; }

/// <summary>
/// Describes how much of <see cref="AimDifficultStrainCount"/> is contributed to by hitcircles or sliders
/// A value closer to 0.0 indicates most of <see cref="AimDifficultStrainCount"/> is contributed by hitcircles
Expand Down
17 changes: 13 additions & 4 deletions osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
if (beatmap.HitObjects.Count == 0)
return new OsuDifficultyAttributes { Mods = mods };

var aim = skills.OfType<Aim>().Single(a => a.IncludeSliders);
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
var aim = skills.OfType<Aim>().Single(a => a.IncludeSliders && !a.AimCheese);
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders && !a.AimCheese);
var aimWithCheese = skills.OfType<Aim>().Single(a => a.IncludeSliders && a.AimCheese);
var speed = skills.OfType<Speed>().Single();
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
var reading = skills.OfType<Reading>().Single();

double aimDifficultyValue = aim.DifficultyValue();
double aimNoSlidersDifficultyValue = aimWithoutSliders.DifficultyValue();
double aimCheeseDifficultyValue = aimWithCheese.DifficultyValue();
double speedDifficultyValue = speed.DifficultyValue();
double readingDifficultyValue = reading.DifficultyValue();

Expand Down Expand Up @@ -68,11 +70,16 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat

double aimRating = calculateAimDifficultyRating(aimDifficultyValue);
double aimNoSlidersRating = calculateAimDifficultyRating(aimNoSlidersDifficultyValue);
double aimCheeseRating = calculateAimDifficultyRating(aimCheeseDifficultyValue);

double sliderFactor = aimDifficultyValue > 0
? aimNoSlidersRating / aimRating
: 1;

double cheeseFactor = aimDifficultyValue > 0
? aimCheeseRating / aimRating
: 1;

double speedRating = calculateDifficultyRating(speedDifficultyValue);
double readingRating = calculateDifficultyRating(readingDifficultyValue);

Expand Down Expand Up @@ -108,6 +115,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
FlashlightDifficulty = flashlightRating,
ReadingDifficulty = readingRating,
SliderFactor = sliderFactor,
CheeseFactor = cheeseFactor,
AimDifficultStrainCount = aimDifficultStrainCount,
SpeedDifficultStrainCount = speedDifficultStrainCount,
ReadingDifficultNoteCount = readingDifficultNoteCount,
Expand Down Expand Up @@ -166,8 +174,9 @@ protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
{
var skills = new List<Skill>
{
new Aim(mods, true),
new Aim(mods, false),
new Aim(mods, true, false),
new Aim(mods, false, false),
new Aim(mods, true, true),
new Speed(mods),
new Reading(mods)
};
Expand Down
59 changes: 56 additions & 3 deletions osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ public class OsuPerformanceCalculator : PerformanceCalculator
private double approachRate;
private double drainRate;

private double? totalDeviation;
private double? speedDeviation;
private double cheeseFactor;

private double aimEstimatedSliderBreaks;
private double speedEstimatedSliderBreaks;
Expand Down Expand Up @@ -150,7 +152,9 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
}

totalDeviation = calculateTotalDeviation(osuAttributes);
speedDeviation = calculateSpeedDeviation(osuAttributes);
cheeseFactor = osuAttributes.CheeseFactor;

double aimValue = computeAimValue(score, osuAttributes);
double speedValue = computeSpeedValue(score, osuAttributes);
Expand Down Expand Up @@ -181,7 +185,7 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s

private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
if (score.Mods.Any(h => h is OsuModAutopilot))
if (score.Mods.Any(h => h is OsuModAutopilot) || totalDeviation == null)
return 0.0;

double aimDifficulty = attributes.AimDifficulty;
Expand All @@ -203,7 +207,8 @@ private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attribut
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, attributes.AimDifficultSliderCount);
}

double sliderNerfFactor = (1 - attributes.SliderFactor) * DiffUtils.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor;
double sliderNerfFactor = (1 - attributes.SliderFactor) * DiffUtils.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3)
+ attributes.SliderFactor;
aimDifficulty *= sliderNerfFactor;
}

Expand All @@ -228,7 +233,11 @@ private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attribut
aimValue *= 1.0 + calculateTraceableBonus(attributes.SliderFactor);
}

aimValue *= accuracy;
double baseNerf = 1 - DiffUtils.Smootherstep(cheeseFactor, 1, 0.9);

double cheeseNerf = baseNerf + (1 - baseNerf) * DiffUtils.Pow(DiffUtils.Erf(23.0 / totalDeviation.Value), 4);

aimValue *= accuracy * cheeseNerf;

return aimValue;
}
Expand Down Expand Up @@ -482,6 +491,50 @@ private double calculateEstimatedSliderBreaks(double topWeightedSliderFactor, Os
return deviation;
}

/// <summary>
/// Estimates the player's total tapping deviation.
/// </summary>
private double? calculateTotalDeviation(OsuDifficultyAttributes attributes)
{
if (totalSuccessfulHits == 0)
return null;

if (!usingClassicSliderAccuracy)
return calculateDeviation(countGreat, countOk, countMeh);

int circleCount = attributes.HitCircleCount;
int missCountCircles = Math.Min(countMiss, circleCount);
int mehCountCircles = Math.Min(countMeh, circleCount - missCountCircles);
int okCountCircles = Math.Min(countOk, circleCount - missCountCircles - mehCountCircles);
int greatCountCircles = Math.Max(0, circleCount - missCountCircles - mehCountCircles - okCountCircles);

// Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s,
// compute the deviation on circles.
if (greatCountCircles > 0)
{
return calculateDeviation(greatCountCircles, okCountCircles, mehCountCircles);
}

// If there are more non-300s than there are circles, compute the deviation on sliders instead.
// Here, all that matters is whether or not the slider was missed, since it is impossible
// to get a 100 or 50 on a slider by mis-tapping it.
int sliderCount = attributes.SliderCount;
int missCountSliders = Math.Min(sliderCount, countMiss - missCountCircles);
int greatCountSliders = sliderCount - missCountSliders;

// We only get here if nothing was hit. In this case, there is no estimate for deviation.
// Note that this is never negative, so checking if this is only equal to 0 makes sense.
if (greatCountSliders == 0)
{
return null;
}

double greatProbabilitySlider = greatCountSliders / (sliderCount + 1.0);
double deviationOnSliders = mehHitWindow / (DiffUtils.SQRT2 * DiffUtils.ErfInv(greatProbabilitySlider));

return deviationOnSliders;
}

// Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty
// https://www.desmos.com/calculator/dmogdhzofn
private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes)
Expand Down
6 changes: 4 additions & 2 deletions osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public class Aim : VariableLengthStrainSkill
{
public readonly bool IncludeSliders;
public readonly bool AimCheese;

public Aim(Mod[] mods, bool includeSliders)
public Aim(Mod[] mods, bool includeSliders, bool aimCheese)
: base(mods)
{
IncludeSliders = includeSliders;
AimCheese = aimCheese;
}

private double currentStrain;
Expand Down Expand Up @@ -62,7 +64,7 @@ private double calculateAdjustedDifficulty(DifficultyHitObject current)

double snapDifficulty = SnapAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skill_multiplier_snap;
double agilityDifficulty = AgilityEvaluator.EvaluateDifficultyOf(current) * skill_multiplier_agility;
double flowDifficulty = FlowAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skill_multiplier_flow;
double flowDifficulty = FlowAimEvaluator.EvaluateDifficultyOf(current, IncludeSliders, AimCheese) * skill_multiplier_flow;

double totalDifficulty = calculateTotalValue(snapDifficulty, agilityDifficulty, flowDifficulty);

Expand Down
Loading