1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 05:32:56 +08:00

Many changes

1) High AR nerf
2) Overlap fix
3) Optimisation
This commit is contained in:
Givikap120 2024-04-07 22:28:55 +03:00
parent 2dbdd4f7d7
commit ae4f0a10ac
3 changed files with 181 additions and 130 deletions

View File

@ -16,22 +16,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{
private const double reading_window_size = 3000;
private const double overlap_multiplier = 1.8;
private const double overlap_multiplier = 1.0;
public static double EvaluateDensityOf(DifficultyHitObject current, bool applyDistanceNerf = true)
{
var currObj = (OsuDifficultyHitObject)current;
double density = 0;
double densityAnglesNerf = -2.5; // we have threshold of 2.5
OsuDifficultyHitObject? prevObj0 = null;
OsuDifficultyHitObject? prevObj1 = null;
OsuDifficultyHitObject? prevObj2 = null;
double prevAngleNerf = 1;
foreach (var loopObj in retrievePastVisibleObjects(currObj).Reverse())
foreach (var readingpObj in currObj.ReadingObjects)
{
var loopObj = readingpObj.HitObject;
if (loopObj.Index < 1)
continue; // Don't look on the first object of the map
@ -48,12 +49,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (prevObj0.IsNull())
prevObj0 = (OsuDifficultyHitObject)loopObj.Previous(0);
if (prevObj1.IsNull())
prevObj1 = (OsuDifficultyHitObject?)loopObj.Previous(1);
if (prevObj2.IsNull())
prevObj2 = (OsuDifficultyHitObject?)loopObj.Previous(2);
// Only if next object is slower, representing break from many notes in a row
if (loopObj.StrainTime > prevObj0.StrainTime)
{
@ -72,92 +67,32 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
density += loopDifficulty;
// Angles nerf
double currAngleNerf = (loopObj.AnglePredictability / 2) + 0.5;
if (loopObj.Angle.IsNotNull() && prevObj0.IsNotNull() && prevObj0.Angle.IsNotNull())
// Apply the nerf only when it's repeated
double angleNerf = Math.Min(currAngleNerf, prevAngleNerf);
// Reduce angles nerf if objects are too apart in time
// Angle nerf is starting being reduced from 200ms (150BPM jump) and it reduced to 0 on 2000ms
//double longIntervalFactor = Math.Clamp(1 - (loopObj.StrainTime - 200) / (2000 - 200), 0, 1);
// Bandaid to fix Rubik's Cube +EZ
double wideness = 0;
if (loopObj.Angle.IsNotNull() && loopObj.Angle.Value > Math.PI * 0.5)
{
double angleDifference = Math.Abs(prevObj0.Angle.Value - loopObj.Angle.Value);
// Goes from 0 to 1 as angle increasing from 90 degrees to 180
wideness = (loopObj.Angle.Value / Math.PI - 0.5) * 2;
// Assume that very low spacing difference means that angles don't matter
if (prevObj0.LazyJumpDistance < OsuDifficultyHitObject.NORMALISED_RADIUS)
angleDifference *= Math.Pow(prevObj0.LazyJumpDistance / OsuDifficultyHitObject.NORMALISED_RADIUS, 2);
if (loopObj.LazyJumpDistance < OsuDifficultyHitObject.NORMALISED_RADIUS)
angleDifference *= Math.Pow(loopObj.LazyJumpDistance / OsuDifficultyHitObject.NORMALISED_RADIUS, 2);
// assume worst-case if no angles
double angleDifference1 = 0;
double angleDifference2 = 0;
// Nerf alternating angles case
if (prevObj1.IsNotNull() && prevObj2.IsNotNull() && prevObj1.Angle.IsNotNull() && prevObj2.Angle.IsNotNull())
{
// Normalized difference
angleDifference1 = Math.Abs(prevObj1.Angle.Value - loopObj.Angle.Value) / Math.PI;
angleDifference2 = Math.Abs(prevObj2.Angle.Value - prevObj0.Angle.Value) / Math.PI;
}
// Will be close to 1 if angleDifference1 and angleDifference2 was both close to 0
double alternatingFactor = Math.Pow((1 - angleDifference1) * (1 - angleDifference2), 2);
// Be sure to nerf only same rhythms
double rhythmFactor = 1 - getRhythmDifference(loopObj.StrainTime, prevObj0.StrainTime); // 0 on different rhythm, 1 on same rhythm
if (prevObj1.IsNotNull())
rhythmFactor *= 1 - getRhythmDifference(prevObj0.StrainTime, prevObj1.StrainTime);
if (prevObj1.IsNotNull() && prevObj2.IsNotNull())
rhythmFactor *= 1 - getRhythmDifference(prevObj1.StrainTime, prevObj2.StrainTime);
// double acuteAngleFactor = 1 - Math.Min(loopObj.Angle.Value, prevObj0.Angle.Value) / Math.PI;
double prevAngleAdjust = Math.Max(angleDifference - angleDifference1, 0);
prevAngleAdjust *= alternatingFactor; // Nerf if alternating
prevAngleAdjust *= rhythmFactor; // Nerf if same rhythms
// prevAngleAdjust *= acuteAngleFactor; // no longer needed?
angleDifference -= prevAngleAdjust;
// Reduce angles nerf if objects are too apart in time
// Angle nerf is starting being reduced from 200ms (150BPM jump) and it reduced to 0 on 2000ms
double longIntervalFactor = Math.Clamp(1 - (loopObj.StrainTime - 200) / (2000 - 200), 0, 1);
// Bandaid to fix Rubik's Cube +EZ
double wideness = 0;
if (loopObj.Angle!.Value > Math.PI * 0.5)
{
// Goes from 0 to 1 as angle increasing from 90 degrees to 180
wideness = (loopObj.Angle.Value / Math.PI - 0.5) * 2;
// Transform into cubic scaling
wideness = 1 - Math.Pow(1 - wideness, 3);
}
// Angle difference will be considered as 2 times lower if angle is wide
angleDifference /= 1 + wideness;
// Current angle nerf. Angle difference more than 15 degrees gets no penalty
double adjustedAngleDifference = Math.Min(Math.PI / 12, angleDifference);
// WARNING - this thing always gives at least 0.5 angle nerf, this is a bug, but removing it completely ruins everything
// Theoretically - this issue is fixable by changing multipliers everywhere,
// but this is not needed because this bug have no drawbacks outside of algorithm not working as intended
double currAngleNerf = Math.Cos(Math.Min(Math.PI / 2, 4 * adjustedAngleDifference));
// Apply the nerf only when it's repeated
double angleNerf = Math.Min(currAngleNerf, prevAngleNerf);
// But only for sharp angles
angleNerf += wideness * (currAngleNerf - angleNerf);
densityAnglesNerf += Math.Min(angleNerf, loopDifficulty);
prevAngleNerf = currAngleNerf;
}
else // Assume worst-case if no angles
{
densityAnglesNerf += loopDifficulty;
// Transform into cubic scaling
wideness = 1 - Math.Pow(1 - wideness, 3);
}
prevObj2 = prevObj1;
prevObj1 = prevObj0;
// But only for sharp angles
angleNerf += wideness * (currAngleNerf - angleNerf);
densityAnglesNerf += Math.Min(angleNerf, loopDifficulty);
prevAngleNerf = currAngleNerf;
prevObj0 = loopObj;
}
@ -171,30 +106,60 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
var currObj = (OsuDifficultyHitObject)current;
double screenOverlapDifficulty = 0;
List<double> overlapDifficulties = new List<double>();
if (currObj.ReadingObjects.Count == 0)
return 0;
foreach (var loopObj in retrievePastVisibleObjects(currObj))
var overlapDifficulties = new List<(OsuDifficultyHitObject HitObject, double Difficulty)>();
// Find initial overlap values
foreach (var loopObj in currObj.ReadingObjects)
{
double lastOverlapness = 0;
foreach (var overlapObj in loopObj.OverlapObjects)
foreach (var overlapObj in loopObj.HitObject.ReadingObjects)
{
if (overlapObj.HitObject.StartTime + overlapObj.HitObject.Preempt <= currObj.StartTime) break;
lastOverlapness = overlapObj.Overlapness;
}
overlapDifficulties.Add(lastOverlapness);
if (lastOverlapness > 0) overlapDifficulties.Add((loopObj.HitObject, lastOverlapness));
}
var sortedDifficulties = overlapDifficulties.OrderByDescending(d => d.Difficulty);
for (int i = 0; i < sortedDifficulties.Count(); i++)
{
var harderObject = sortedDifficulties.ElementAt(i);
// Look for all easier objects
for (int j = i + 1; j < sortedDifficulties.Count(); j++)
{
var easierObject = sortedDifficulties.ElementAt(j);
// Get the overlap value
double overlapValue;
// OverlapValues dict only contains prev objects, so be sure to use right object
if (harderObject.HitObject.Index > easierObject.HitObject.Index)
overlapValue = harderObject.HitObject.OverlapValues[easierObject.HitObject];
else
overlapValue = easierObject.HitObject.OverlapValues[harderObject.HitObject];
// Nerf easier object if it overlaps in the same place as hard one
easierObject.Difficulty *= Math.Pow(1 - overlapValue, 2);
}
}
const double decay_weight = 0.5;
double weight = 1.0;
foreach (double difficulty in overlapDifficulties.OrderDescending())
foreach (var diffObject in sortedDifficulties.OrderByDescending(d => d.Difficulty))
{
screenOverlapDifficulty += difficulty * weight;
// Add weighted difficulty
screenOverlapDifficulty += Math.Max(0, diffObject.Difficulty - 0.5) * weight;
weight *= decay_weight;
}
return overlap_multiplier * Math.Max(0, screenOverlapDifficulty - 1.2);
return overlap_multiplier * Math.Max(0, screenOverlapDifficulty);
}
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
@ -304,23 +269,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return velocityChangeFactor;
}
// Returns a list of objects that are visible on screen at
// the point in time at which the current object becomes visible.
private static IEnumerable<OsuDifficultyHitObject> retrievePastVisibleObjects(OsuDifficultyHitObject current)
{
for (int i = 0; i < current.Index; i++)
{
OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Previous(i);
if (hitObject.IsNull() ||
current.StartTime - hitObject.StartTime > reading_window_size ||
hitObject.StartTime < current.StartTime - current.Preempt)
break;
yield return hitObject;
}
}
private static double getTimeNerfFactor(double deltaTime)
{
return Math.Clamp(2 - deltaTime / (reading_window_size / 2), 0, 1);
@ -393,7 +341,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
value = Math.Exp(9.07583 - 80.0 * preempt / 3);
// EDIT: looks like AR11 getting a bit overnerfed in comparison to other ARs, so i will increase the difference
return Math.Pow(value, 1.4);
return Math.Pow(value, 1.25);
}
}
}

View File

@ -103,10 +103,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary>
public double Density { get; private set; }
/// <summary>
/// Predictabiliy of the angle. Gives high values only in exceptionally repetitive patterns.
/// </summary>
public double AnglePredictability { get; private set; }
/// <summary>
/// Objects that was visible after the note was hit together with cumulative overlapping difficulty. Saved for optimization to avoid O(x^4) time complexity.
/// </summary>
public IList<OverlapObject> OverlapObjects { get; private set; }
public IList<ReadingObject> ReadingObjects { get; private set; }
/// <summary>
/// Objects that was visible after the note was hit together with cumulative overlapping difficulty. Saved for optimization to avoid O(x^4) time complexity.
/// </summary>
public IDictionary<OsuDifficultyHitObject, double> OverlapValues { get; private set; }
/// <summary>
/// Time in ms between appearence of this <see cref="OsuDifficultyHitObject"/> and moment to click on it.
@ -152,15 +162,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
setDistances(clockRate);
AnglePredictability = calculateAnglePredictability();
OverlapValues = new Dictionary<OsuDifficultyHitObject, double>();
ReadingObjects = getOverlapObjects();
RhythmDifficulty = RhythmEvaluator.EvaluateDifficultyOf(this);
Density = ReadingEvaluator.EvaluateDensityOf(this);
OverlapObjects = getOverlapObjects();
}
private List<OverlapObject> getOverlapObjects()
private List<ReadingObject> getOverlapObjects()
{
List<OverlapObject> overlapObjects = new List<OverlapObject>();
List<ReadingObject> overlapObjects = new List<ReadingObject>();
double totalOverlapnessDifficulty = 0;
double currentTime = DeltaTime;
@ -174,6 +187,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
// Overlapness with this object
double currentOverlapness = calculateOverlapness(this, loopObj);
// Save it for future use
OverlapValues[loopObj] = currentOverlapness;
if (prevObject.Angle.IsNull())
{
currentTime += prevObject.DeltaTime;
@ -184,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
double angle = (double)prevObject.Angle;
// Overlapness between current and prev to make streams have 0 buff
double instantOverlapness = 0.5 + calculateOverlapness(prevObject, loopObj);
double instantOverlapness = 0.5 + prevObject.OverlapValues[loopObj];
// Nerf overlaps on wide angles
double angleFactor = 1;
@ -192,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
instantOverlapness = Math.Min(1, instantOverlapness * angleFactor); // wide angles are more predictable
currentOverlapness *= (1 - instantOverlapness) * 2; // wide angles will have close-to-zero buff
currentOverlapness *= OpacityAt(loopObj.BaseObject.StartTime, false);
currentOverlapness *= getOpacitiyMultiplier(loopObj); // Increase stability by using opacity
// Control overlap repetitivness
if (currentOverlapness > 0)
@ -240,13 +256,28 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
}
totalOverlapnessDifficulty += currentOverlapness;
overlapObjects.Add(new OverlapObject(loopObj, totalOverlapnessDifficulty));
overlapObjects.Add(new ReadingObject(loopObj, totalOverlapnessDifficulty));
prevObject = loopObj;
}
return overlapObjects;
}
private double getOpacitiyMultiplier(OsuDifficultyHitObject loopObj)
{
const double threshold = 0.3;
// Get raw opacity
double opacity = OpacityAt(loopObj.BaseObject.StartTime, false);
opacity = Math.Min(1, opacity + threshold); // object with opacity 0.7 are still perfectly visible
opacity -= threshold; // return opacity 0 objects back to 0
opacity /= 1 - threshold; // fix scaling to be 0-1 again
opacity = Math.Sqrt(opacity); // change curve
return opacity;
}
private static double getSimilarity(double timeA, double timeB)
{
double similarity = Math.Min(timeA, timeB) / Math.Max(timeA, timeB);
@ -304,8 +335,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
OsuDifficultyHitObject hitObject = (OsuDifficultyHitObject)current.Previous(i);
if (hitObject.IsNull() ||
// (hitObject.StartTime - current.StartTime) > reading_window_size ||
//current.StartTime < hitObject.StartTime - hitObject.Preempt)
hitObject.StartTime < current.StartTime - current.Preempt)
break;
@ -313,6 +342,80 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
}
}
private double calculateAnglePredictability()
{
OsuDifficultyHitObject? prevObj0 = (OsuDifficultyHitObject?)Previous(0);
OsuDifficultyHitObject? prevObj1 = (OsuDifficultyHitObject?)Previous(1);
OsuDifficultyHitObject? prevObj2 = (OsuDifficultyHitObject?)Previous(2);
if (Angle.IsNull() || prevObj0.IsNull() || prevObj0.Angle.IsNull())
return 1.0;
double angleDifference = Math.Abs(prevObj0.Angle.Value - Angle.Value);
// Assume that very low spacing difference means that angles don't matter
if (prevObj0.LazyJumpDistance < NORMALISED_RADIUS)
angleDifference *= Math.Pow(prevObj0.LazyJumpDistance / NORMALISED_RADIUS, 2);
if (LazyJumpDistance < NORMALISED_RADIUS)
angleDifference *= Math.Pow(LazyJumpDistance / NORMALISED_RADIUS, 2);
// assume worst-case if no angles
double angleDifference1 = 0;
double angleDifference2 = 0;
// Nerf alternating angles case
if (prevObj1.IsNotNull() && prevObj2.IsNotNull() && prevObj1.Angle.IsNotNull() && prevObj2.Angle.IsNotNull())
{
// Normalized difference
angleDifference1 = Math.Abs(prevObj1.Angle.Value - Angle.Value) / Math.PI;
angleDifference2 = Math.Abs(prevObj2.Angle.Value - prevObj0.Angle.Value) / Math.PI;
}
// Will be close to 1 if angleDifference1 and angleDifference2 was both close to 0
double alternatingFactor = Math.Pow((1 - angleDifference1) * (1 - angleDifference2), 2);
// Be sure to nerf only same rhythms
double rhythmFactor = 1 - getRhythmDifference(StrainTime, prevObj0.StrainTime); // 0 on different rhythm, 1 on same rhythm
if (prevObj1.IsNotNull())
rhythmFactor *= 1 - getRhythmDifference(prevObj0.StrainTime, prevObj1.StrainTime);
if (prevObj1.IsNotNull() && prevObj2.IsNotNull())
rhythmFactor *= 1 - getRhythmDifference(prevObj1.StrainTime, prevObj2.StrainTime);
double prevAngleAdjust = Math.Max(angleDifference - angleDifference1, 0);
prevAngleAdjust *= alternatingFactor; // Nerf if alternating
prevAngleAdjust *= rhythmFactor; // Nerf if same rhythms
angleDifference -= prevAngleAdjust;
// Bandaid to fix Rubik's Cube +EZ
double wideness = 0;
if (Angle!.Value > Math.PI * 0.5)
{
// Goes from 0 to 1 as angle increasing from 90 degrees to 180
wideness = (Angle.Value / Math.PI - 0.5) * 2;
// Transform into cubic scaling
wideness = 1 - Math.Pow(1 - wideness, 3);
}
// Angle difference will be considered as 2 times lower if angle is wide
angleDifference /= 1 + wideness;
// Current angle nerf. Angle difference more than 15 degrees gets no penalty
double adjustedAngleDifference = Math.Min(Math.PI / 12, angleDifference);
// WARNING - this thing always gives at least 0.5 angle nerf, this is a bug, but removing it completely ruins everything
// Theoretically - this issue is fixable by changing multipliers everywhere,
// but this is not needed because this bug have no drawbacks outside of algorithm not working as intended
double currAngleNerf = Math.Cos(Math.Min(Math.PI / 2, 4 * adjustedAngleDifference));
return (currAngleNerf - 0.5) * 2;
}
private static double getRhythmDifference(double t1, double t2) => 1 - Math.Min(t1, t2) / Math.Max(t1, t2);
public double OpacityAt(double time, bool hidden)
{
if (time > BaseObject.StartTime)
@ -535,12 +638,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
return pos;
}
public struct OverlapObject
public struct ReadingObject
{
public OsuDifficultyHitObject HitObject;
public double Overlapness;
public OverlapObject(OsuDifficultyHitObject hitObject, double overlapness)
public ReadingObject(OsuDifficultyHitObject hitObject, double overlapness)
{
HitObject = hitObject;
Overlapness = overlapness;

View File

@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private bool adjustHighAR;
private double currentStrain;
private double skillMultiplier => 8.9;
private double skillMultiplier => 7;
private double defaultValueMultiplier => 25;
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * StrainDecay(time - current.Previous(0).StartTime);
@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public class HighARSpeedComponent : OsuStrainSkill
{
private double skillMultiplier => 520;
private double skillMultiplier => 7 * 0.017;
protected override double StrainDecayBase => 0.3;
private double currentStrain;