mirror of
https://github.com/osukey/osukey.git
synced 2025-08-03 22:56:36 +09:00
Add difficulty calculation
Adds base classes for difficulty calculations, hooks them up with carousel container, and adds a port of the osu difficulty calculator.
This commit is contained in:
@ -108,7 +108,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 scaleToCircle => new Vector2(circle.Scale * circle.DrawWidth / DrawWidth) * 0.95f;
|
||||
private Vector2 scaleToCircle => (circle.Scale * circle.DrawWidth / DrawWidth) * 0.95f;
|
||||
|
||||
private float spinsPerMinuteNeeded = 100 + (5 * 15); //TODO: read per-map OD and place it on the 5
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Modes.Osu.Objects
|
||||
{
|
||||
public class HitCircle : OsuHitObject
|
||||
{
|
||||
public override HitObjectType Type => HitObjectType.Circle;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
|
||||
public float Scale { get; set; } = 1;
|
||||
|
||||
public abstract HitObjectType Type { get; }
|
||||
|
||||
public override void SetDefaultsFromBeatmap(Beatmap beatmap)
|
||||
{
|
||||
base.SetDefaultsFromBeatmap(beatmap);
|
||||
@ -36,14 +38,12 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
[Flags]
|
||||
public enum HitObjectType
|
||||
{
|
||||
Circle = 1,
|
||||
Slider = 2,
|
||||
NewCombo = 4,
|
||||
CircleNewCombo = 5,
|
||||
SliderNewCombo = 6,
|
||||
Spinner = 8,
|
||||
Circle = 1 << 0,
|
||||
Slider = 1 << 1,
|
||||
NewCombo = 1 << 2,
|
||||
Spinner = 1 << 3,
|
||||
ColourHax = 122,
|
||||
Hold = 128,
|
||||
ManiaLong = 128,
|
||||
Hold = 1 << 7,
|
||||
SliderTick = 1 << 8,
|
||||
}
|
||||
}
|
||||
|
200
osu.Game.Modes.Osu/Objects/OsuHitObjectDifficulty.cs
Normal file
200
osu.Game.Modes.Osu/Objects/OsuHitObjectDifficulty.cs
Normal file
@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using OpenTK;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Modes.Osu.Objects
|
||||
{
|
||||
class OsuHitObjectDifficulty
|
||||
{
|
||||
/// <summary>
|
||||
/// Factor by how much speed / aim strain decays per second.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These values are results of tweaking a lot and taking into account general feedback.
|
||||
/// Opinionated observation: Speed is easier to maintain than accurate jumps.
|
||||
/// </remarks>
|
||||
internal static readonly double[] DECAY_BASE = { 0.3, 0.15 };
|
||||
|
||||
/// <summary>
|
||||
/// Pseudo threshold values to distinguish between "singles" and "streams"
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Of course the border can not be defined clearly, therefore the algorithm has a smooth transition between those values.
|
||||
/// They also are based on tweaking and general feedback.
|
||||
/// </remarks>
|
||||
private const double STREAM_SPACING_TRESHOLD = 110,
|
||||
SINGLE_SPACING_TRESHOLD = 125;
|
||||
|
||||
/// <summary>
|
||||
/// Scaling values for weightings to keep aim and speed difficulty in balance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Found from testing a very large map pool (containing all ranked maps) and keeping the average values the same.
|
||||
/// </remarks>
|
||||
private static readonly double[] SPACING_WEIGHT_SCALING = { 1400, 26.25 };
|
||||
|
||||
/// <summary>
|
||||
/// Almost the normed diameter of a circle (104 osu pixel). That is -after- position transforming.
|
||||
/// </summary>
|
||||
private const double ALMOST_DIAMETER = 90;
|
||||
|
||||
internal OsuHitObject BaseHitObject;
|
||||
internal double[] Strains = { 1, 1 };
|
||||
|
||||
internal int MaxCombo = 1;
|
||||
|
||||
private Vector2 normalizedStartPosition;
|
||||
private Vector2 normalizedEndPosition;
|
||||
private float lazySliderLength;
|
||||
|
||||
internal OsuHitObjectDifficulty(OsuHitObject baseHitObject)
|
||||
{
|
||||
BaseHitObject = baseHitObject;
|
||||
float circleRadius = baseHitObject.Scale * 64;
|
||||
|
||||
Slider slider = BaseHitObject as Slider;
|
||||
if (slider != null)
|
||||
MaxCombo += slider.Ticks.Count();
|
||||
|
||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
float scalingFactor = (52.0f / circleRadius);
|
||||
if (circleRadius < 30)
|
||||
{
|
||||
float smallCircleBonus = Math.Min(30.0f - circleRadius, 5.0f) / 50.0f;
|
||||
scalingFactor *= 1.0f + smallCircleBonus;
|
||||
}
|
||||
|
||||
normalizedStartPosition = BaseHitObject.StackedPosition * scalingFactor;
|
||||
|
||||
lazySliderLength = 0;
|
||||
|
||||
// Calculate approximation of lazy movement on the slider
|
||||
if (slider != null)
|
||||
{
|
||||
float sliderFollowCircleRadius = circleRadius * 3; // Not sure if this is correct, but here we do not need 100% exact values. This comes pretty darn close in my tests.
|
||||
|
||||
// For simplifying this step we use actual osu! coordinates and simply scale the length, that we obtain by the ScalingFactor later
|
||||
Vector2 cursorPos = baseHitObject.StackedPosition;
|
||||
|
||||
Action<Vector2> addSliderVertex = delegate (Vector2 pos)
|
||||
{
|
||||
Vector2 difference = pos - cursorPos;
|
||||
float distance = difference.Length;
|
||||
|
||||
// Did we move away too far?
|
||||
if (distance > sliderFollowCircleRadius)
|
||||
{
|
||||
// Yep, we need to move the cursor
|
||||
difference.Normalize(); // Obtain the direction of difference. We do no longer need the actual difference
|
||||
distance -= sliderFollowCircleRadius;
|
||||
cursorPos += difference * distance; // We move the cursor just as far as needed to stay in the follow circle
|
||||
lazySliderLength += distance;
|
||||
}
|
||||
};
|
||||
|
||||
// Actual computation of the first lazy curve
|
||||
foreach (var tick in slider.Ticks)
|
||||
addSliderVertex(tick.StackedPosition);
|
||||
|
||||
addSliderVertex(baseHitObject.StackedEndPosition);
|
||||
|
||||
lazySliderLength *= scalingFactor;
|
||||
normalizedEndPosition = cursorPos * scalingFactor;
|
||||
}
|
||||
// We have a normal HitCircle or a spinner
|
||||
else
|
||||
normalizedEndPosition = normalizedStartPosition;
|
||||
}
|
||||
|
||||
internal void CalculateStrains(OsuHitObjectDifficulty previousHitObject, double timeRate)
|
||||
{
|
||||
calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Speed, timeRate);
|
||||
calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Aim, timeRate);
|
||||
}
|
||||
|
||||
// Caution: The subjective values are strong with this one
|
||||
private static double spacingWeight(double distance, OsuDifficultyCalculator.DifficultyType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case OsuDifficultyCalculator.DifficultyType.Speed:
|
||||
if (distance > SINGLE_SPACING_TRESHOLD)
|
||||
return 2.5;
|
||||
else if (distance > STREAM_SPACING_TRESHOLD)
|
||||
return 1.6 + 0.9 * (distance - STREAM_SPACING_TRESHOLD) / (SINGLE_SPACING_TRESHOLD - STREAM_SPACING_TRESHOLD);
|
||||
else if (distance > ALMOST_DIAMETER)
|
||||
return 1.2 + 0.4 * (distance - ALMOST_DIAMETER) / (STREAM_SPACING_TRESHOLD - ALMOST_DIAMETER);
|
||||
else if (distance > ALMOST_DIAMETER / 2)
|
||||
return 0.95 + 0.25 * (distance - (ALMOST_DIAMETER / 2)) / (ALMOST_DIAMETER / 2);
|
||||
else
|
||||
return 0.95;
|
||||
|
||||
case OsuDifficultyCalculator.DifficultyType.Aim:
|
||||
return Math.Pow(distance, 0.99);
|
||||
}
|
||||
|
||||
Debug.Assert(false, "Invalid osu difficulty hit object type.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void calculateSpecificStrain(OsuHitObjectDifficulty previousHitObject, OsuDifficultyCalculator.DifficultyType type, double timeRate)
|
||||
{
|
||||
double addition = 0;
|
||||
double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
|
||||
double decay = Math.Pow(DECAY_BASE[(int)type], timeElapsed / 1000);
|
||||
|
||||
if (BaseHitObject.Type == HitObjectType.Spinner)
|
||||
{
|
||||
// Do nothing for spinners
|
||||
}
|
||||
else if (BaseHitObject.Type == HitObjectType.Slider)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case OsuDifficultyCalculator.DifficultyType.Speed:
|
||||
|
||||
// For speed strain we treat the whole slider as a single spacing entity, since "Speed" is about how hard it is to click buttons fast.
|
||||
// The spacing weight exists to differentiate between being able to easily alternate or having to single.
|
||||
addition =
|
||||
spacingWeight(previousHitObject.lazySliderLength +
|
||||
DistanceTo(previousHitObject), type) *
|
||||
SPACING_WEIGHT_SCALING[(int)type];
|
||||
|
||||
break;
|
||||
case OsuDifficultyCalculator.DifficultyType.Aim:
|
||||
|
||||
// For Aim strain we treat each slider segment and the jump after the end of the slider as separate jumps, since movement-wise there is no difference
|
||||
// to multiple jumps.
|
||||
addition =
|
||||
(
|
||||
spacingWeight(previousHitObject.lazySliderLength, type) +
|
||||
spacingWeight(DistanceTo(previousHitObject), type)
|
||||
) *
|
||||
SPACING_WEIGHT_SCALING[(int)type];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (BaseHitObject.Type == HitObjectType.Circle)
|
||||
{
|
||||
addition = spacingWeight(DistanceTo(previousHitObject), type) * SPACING_WEIGHT_SCALING[(int)type];
|
||||
}
|
||||
|
||||
// Scale addition by the time, that elapsed. Filter out HitObjects that are too close to be played anyway to avoid crazy values by division through close to zero.
|
||||
// You will never find maps that require this amongst ranked maps.
|
||||
addition /= Math.Max(timeElapsed, 50);
|
||||
|
||||
Strains[(int)type] = previousHitObject.Strains[(int)type] * decay + addition;
|
||||
}
|
||||
|
||||
internal double DistanceTo(OsuHitObjectDifficulty other)
|
||||
{
|
||||
// Scale the distance by circle size.
|
||||
return (normalizedStartPosition - other.normalizedEndPosition).Length;
|
||||
}
|
||||
}
|
||||
}
|
@ -110,6 +110,8 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override HitObjectType Type => HitObjectType.Slider;
|
||||
}
|
||||
|
||||
public enum CurveTypes
|
||||
|
@ -5,5 +5,7 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
public class SliderTick : OsuHitObject
|
||||
{
|
||||
public int RepeatIndex { get; set; }
|
||||
|
||||
public override HitObjectType Type => HitObjectType.SliderTick;
|
||||
}
|
||||
}
|
||||
|
@ -10,5 +10,7 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
public double Length;
|
||||
|
||||
public override double EndTime => StartTime + Length;
|
||||
|
||||
public override HitObjectType Type => HitObjectType.Spinner;
|
||||
}
|
||||
}
|
||||
|
193
osu.Game.Modes.Osu/OsuDifficultyCalculator.cs
Normal file
193
osu.Game.Modes.Osu/OsuDifficultyCalculator.cs
Normal file
@ -0,0 +1,193 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Modes.Osu.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Modes.Objects;
|
||||
|
||||
namespace osu.Game.Modes.Osu
|
||||
{
|
||||
public class OsuDifficultyCalculator : DifficultyCalculator<OsuHitObject>
|
||||
{
|
||||
private const double STAR_SCALING_FACTOR = 0.0675;
|
||||
private const double EXTREME_SCALING_FACTOR = 0.5;
|
||||
|
||||
protected override PlayMode PlayMode => PlayMode.Osu;
|
||||
|
||||
/// <summary>
|
||||
/// HitObjects are stored as a member variable.
|
||||
/// </summary>
|
||||
internal List<OsuHitObjectDifficulty> DifficultyHitObjects = new List<OsuHitObjectDifficulty>();
|
||||
|
||||
public OsuDifficultyCalculator(Beatmap beatmap) : base(beatmap)
|
||||
{
|
||||
}
|
||||
|
||||
protected override HitObjectConverter<OsuHitObject> Converter => new OsuHitObjectConverter();
|
||||
|
||||
protected override void PreprocessHitObjects()
|
||||
{
|
||||
foreach (var h in Objects)
|
||||
if (h.Type == HitObjectType.Slider)
|
||||
((Slider)h).Curve.Calculate();
|
||||
}
|
||||
|
||||
protected override double ComputeDifficulty(Dictionary<String, String> categoryDifficulty)
|
||||
{
|
||||
// Fill our custom DifficultyHitObject class, that carries additional information
|
||||
DifficultyHitObjects.Clear();
|
||||
|
||||
foreach (var hitObject in Objects)
|
||||
DifficultyHitObjects.Add(new OsuHitObjectDifficulty(hitObject));
|
||||
|
||||
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
|
||||
DifficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
|
||||
|
||||
if (!CalculateStrainValues()) return 0;
|
||||
|
||||
double speedDifficulty = CalculateDifficulty(DifficultyType.Speed);
|
||||
double aimDifficulty = CalculateDifficulty(DifficultyType.Aim);
|
||||
|
||||
// OverallDifficulty is not considered in this algorithm and neither is HpDrainRate. That means, that in this form the algorithm determines how hard it physically is
|
||||
// to play the map, assuming, that too much of an error will not lead to a death.
|
||||
// It might be desirable to include OverallDifficulty into map difficulty, but in my personal opinion it belongs more to the weighting of the actual peformance
|
||||
// and is superfluous in the beatmap difficulty rating.
|
||||
// If it were to be considered, then I would look at the hit window of normal HitCircles only, since Sliders and Spinners are (almost) "free" 300s and take map length
|
||||
// into account as well.
|
||||
|
||||
// The difficulty can be scaled by any desired metric.
|
||||
// In osu!tp it gets squared to account for the rapid increase in difficulty as the limit of a human is approached. (Of course it also gets scaled afterwards.)
|
||||
// It would not be suitable for a star rating, therefore:
|
||||
|
||||
// The following is a proposal to forge a star rating from 0 to 5. It consists of taking the square root of the difficulty, since by simply scaling the easier
|
||||
// 5-star maps would end up with one star.
|
||||
double speedStars = Math.Sqrt(speedDifficulty) * STAR_SCALING_FACTOR;
|
||||
double aimStars = Math.Sqrt(aimDifficulty) * STAR_SCALING_FACTOR;
|
||||
|
||||
if (categoryDifficulty != null)
|
||||
{
|
||||
categoryDifficulty.Add("Aim", aimStars.ToString("0.00"));
|
||||
categoryDifficulty.Add("Speed", speedStars.ToString("0.00"));
|
||||
|
||||
double hitWindow300 = 30/*HitObjectManager.HitWindow300*/ / TimeRate;
|
||||
double preEmpt = 450/*HitObjectManager.PreEmpt*/ / TimeRate;
|
||||
|
||||
categoryDifficulty.Add("OD", (-(hitWindow300 - 80.0) / 6.0).ToString("0.00"));
|
||||
categoryDifficulty.Add("AR", (preEmpt > 1200.0 ? -(preEmpt - 1800.0) / 120.0 : -(preEmpt - 1200.0) / 150.0 + 5.0).ToString("0.00"));
|
||||
|
||||
int maxCombo = 0;
|
||||
foreach (OsuHitObjectDifficulty hitObject in DifficultyHitObjects)
|
||||
maxCombo += hitObject.MaxCombo;
|
||||
|
||||
categoryDifficulty.Add("Max combo", maxCombo.ToString());
|
||||
}
|
||||
|
||||
// Again, from own observations and from the general opinion of the community a map with high speed and low aim (or vice versa) difficulty is harder,
|
||||
// than a map with mediocre difficulty in both. Therefore we can not just add both difficulties together, but will introduce a scaling that favors extremes.
|
||||
double starRating = speedStars + aimStars + Math.Abs(speedStars - aimStars) * EXTREME_SCALING_FACTOR;
|
||||
// Another approach to this would be taking Speed and Aim separately to a chosen power, which again would be equivalent. This would be more convenient if
|
||||
// the hit window size is to be considered as well.
|
||||
|
||||
// Note: The star rating is tuned extremely tight! Airman (/b/104229) and Freedom Dive (/b/126645), two of the hardest ranked maps, both score ~4.66 stars.
|
||||
// Expect the easier kind of maps that officially get 5 stars to obtain around 2 by this metric. The tutorial still scores about half a star.
|
||||
// Tune by yourself as you please. ;)
|
||||
|
||||
return starRating;
|
||||
}
|
||||
|
||||
protected bool CalculateStrainValues()
|
||||
{
|
||||
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
|
||||
List<OsuHitObjectDifficulty>.Enumerator hitObjectsEnumerator = DifficultyHitObjects.GetEnumerator();
|
||||
|
||||
if (!hitObjectsEnumerator.MoveNext()) return false;
|
||||
|
||||
OsuHitObjectDifficulty currentHitObject = hitObjectsEnumerator.Current;
|
||||
OsuHitObjectDifficulty nextHitObject;
|
||||
|
||||
// First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject.
|
||||
while (hitObjectsEnumerator.MoveNext())
|
||||
{
|
||||
nextHitObject = hitObjectsEnumerator.Current;
|
||||
nextHitObject.CalculateStrains(currentHitObject, TimeRate);
|
||||
currentHitObject = nextHitObject;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
|
||||
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
|
||||
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
|
||||
/// </summary>
|
||||
protected const double STRAIN_STEP = 400;
|
||||
|
||||
/// <summary>
|
||||
/// The weighting of each strain value decays to this number * it's previous value
|
||||
/// </summary>
|
||||
protected const double DECAY_WEIGHT = 0.9;
|
||||
|
||||
protected double CalculateDifficulty(DifficultyType type)
|
||||
{
|
||||
double actualStrainStep = STRAIN_STEP * TimeRate;
|
||||
|
||||
// Find the highest strain value within each strain step
|
||||
List<double> highestStrains = new List<double>();
|
||||
double intervalEndTime = actualStrainStep;
|
||||
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
|
||||
|
||||
OsuHitObjectDifficulty previousHitObject = null;
|
||||
foreach (OsuHitObjectDifficulty hitObject in DifficultyHitObjects)
|
||||
{
|
||||
// While we are beyond the current interval push the currently available maximum to our strain list
|
||||
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
|
||||
{
|
||||
highestStrains.Add(maximumStrain);
|
||||
|
||||
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
|
||||
// until the beginning of the next interval.
|
||||
if (previousHitObject == null)
|
||||
{
|
||||
maximumStrain = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
double decay = Math.Pow(OsuHitObjectDifficulty.DECAY_BASE[(int)type], (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
|
||||
maximumStrain = previousHitObject.Strains[(int)type] * decay;
|
||||
}
|
||||
|
||||
// Go to the next time interval
|
||||
intervalEndTime += actualStrainStep;
|
||||
}
|
||||
|
||||
// Obtain maximum strain
|
||||
maximumStrain = Math.Max(hitObject.Strains[(int)type], maximumStrain);
|
||||
|
||||
previousHitObject = hitObject;
|
||||
}
|
||||
|
||||
// Build the weighted sum over the highest strains for each interval
|
||||
double difficulty = 0;
|
||||
double weight = 1;
|
||||
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
|
||||
|
||||
foreach (double strain in highestStrains)
|
||||
{
|
||||
difficulty += weight * strain;
|
||||
weight *= DECAY_WEIGHT;
|
||||
}
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
// Those values are used as array indices. Be careful when changing them!
|
||||
public enum DifficultyType : int
|
||||
{
|
||||
Speed = 0,
|
||||
Aim,
|
||||
};
|
||||
}
|
||||
}
|
@ -40,6 +40,8 @@ namespace osu.Game.Modes.Osu
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor(int hitObjectCount) => new OsuScoreProcessor(hitObjectCount);
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new OsuDifficultyCalculator(beatmap);
|
||||
|
||||
protected override PlayMode PlayMode => PlayMode.Osu;
|
||||
}
|
||||
}
|
||||
|
@ -66,9 +66,11 @@
|
||||
<Compile Include="Objects\Drawables\Pieces\TrianglesPiece.cs" />
|
||||
<Compile Include="Objects\Drawables\Pieces\SliderBall.cs" />
|
||||
<Compile Include="Objects\Drawables\Pieces\SliderBody.cs" />
|
||||
<Compile Include="Objects\OsuHitObjectDifficulty.cs" />
|
||||
<Compile Include="Objects\OsuHitObjectParser.cs" />
|
||||
<Compile Include="Objects\SliderCurve.cs" />
|
||||
<Compile Include="Objects\SliderTick.cs" />
|
||||
<Compile Include="OsuDifficultyCalculator.cs" />
|
||||
<Compile Include="OsuScore.cs" />
|
||||
<Compile Include="OsuScoreProcessor.cs" />
|
||||
<Compile Include="UI\OsuComboCounter.cs" />
|
||||
|
Reference in New Issue
Block a user