mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 12:22:56 +08:00
Merge branch 'master' into taiko-fixes
This commit is contained in:
commit
4087b638cf
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set { Curve.Distance = value; }
|
||||
}
|
||||
|
||||
public List<List<SampleInfo>> RepeatSamples { get; set; } = new List<List<SampleInfo>>();
|
||||
public List<SampleInfoList> RepeatSamples { get; set; } = new List<SampleInfoList>();
|
||||
public int RepeatCount { get; set; } = 1;
|
||||
|
||||
private int stackHeight;
|
||||
@ -117,12 +117,12 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
StackHeight = StackHeight,
|
||||
Scale = Scale,
|
||||
ComboColour = ComboColour,
|
||||
Samples = Samples.Select(s => new SampleInfo
|
||||
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
|
||||
{
|
||||
Bank = s.Bank,
|
||||
Name = @"slidertick",
|
||||
Volume = s.Volume
|
||||
}).ToList()
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -66,9 +66,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
var distanceData = obj as IHasDistance;
|
||||
var repeatsData = obj as IHasRepeats;
|
||||
var endTimeData = obj as IHasEndTime;
|
||||
var curveData = obj as IHasCurve;
|
||||
|
||||
// Old osu! used hit sounding to determine various hit type information
|
||||
List<SampleInfo> samples = obj.Samples;
|
||||
SampleInfoList samples = obj.Samples;
|
||||
|
||||
bool strong = samples.Any(s => s.Name == SampleInfo.HIT_FINISH);
|
||||
|
||||
@ -102,16 +103,35 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
|
||||
if (tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
|
||||
{
|
||||
List<SampleInfoList> allSamples = curveData != null ? curveData.RepeatSamples : new List<SampleInfoList>(new[] { samples });
|
||||
|
||||
int i = 0;
|
||||
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
|
||||
{
|
||||
// Todo: This should generate different type of hits (including strongs)
|
||||
// depending on hitobject sound additions (not implemented fully yet)
|
||||
yield return new CentreHit
|
||||
SampleInfoList currentSamples = allSamples[i];
|
||||
bool isRim = currentSamples.Any(s => s.Name == SampleInfo.HIT_CLAP || s.Name == SampleInfo.HIT_WHISTLE);
|
||||
strong = currentSamples.Any(s => s.Name == SampleInfo.HIT_FINISH);
|
||||
|
||||
if (isRim)
|
||||
{
|
||||
StartTime = j,
|
||||
Samples = obj.Samples,
|
||||
IsStrong = strong,
|
||||
};
|
||||
yield return new RimHit
|
||||
{
|
||||
StartTime = j,
|
||||
Samples = currentSamples,
|
||||
IsStrong = strong
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new CentreHit
|
||||
{
|
||||
StartTime = j,
|
||||
Samples = currentSamples,
|
||||
IsStrong = strong,
|
||||
};
|
||||
}
|
||||
|
||||
i = (i + 1) % allSamples.Count;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -82,12 +82,12 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
TickSpacing = tickSpacing,
|
||||
StartTime = t,
|
||||
IsStrong = IsStrong,
|
||||
Samples = Samples.Select(s => new SampleInfo
|
||||
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
|
||||
{
|
||||
Bank = s.Bank,
|
||||
Name = @"slidertick",
|
||||
Volume = s.Volume
|
||||
}).ToList()
|
||||
}))
|
||||
});
|
||||
|
||||
first = false;
|
||||
|
127
osu.Game.Rulesets.Taiko/Objects/TaikoHitObjectDifficulty.cs
Normal file
127
osu.Game.Rulesets.Taiko/Objects/TaikoHitObjectDifficulty.cs
Normal file
@ -0,0 +1,127 @@
|
||||
// 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.Rulesets.Taiko.Objects
|
||||
{
|
||||
internal class TaikoHitObjectDifficulty
|
||||
{
|
||||
/// <summary>
|
||||
/// Factor by how much individual / overall strain decays per second.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These values are results of tweaking a lot and taking into account general feedback.
|
||||
/// </remarks>
|
||||
internal const double DECAY_BASE = 0.30;
|
||||
|
||||
private const double type_change_bonus = 0.75;
|
||||
private const double rhythm_change_bonus = 1.0;
|
||||
private const double rhythm_change_base_threshold = 0.2;
|
||||
private const double rhythm_change_base = 2.0;
|
||||
|
||||
internal TaikoHitObject BaseHitObject;
|
||||
|
||||
/// <summary>
|
||||
/// Measures note density in a way
|
||||
/// </summary>
|
||||
internal double Strain = 1;
|
||||
|
||||
private double timeElapsed;
|
||||
private int sameTypeSince = 1;
|
||||
|
||||
private bool isRim => BaseHitObject is RimHit;
|
||||
|
||||
public TaikoHitObjectDifficulty(TaikoHitObject baseHitObject)
|
||||
{
|
||||
BaseHitObject = baseHitObject;
|
||||
}
|
||||
|
||||
internal void CalculateStrains(TaikoHitObjectDifficulty previousHitObject, double timeRate)
|
||||
{
|
||||
// Rather simple, but more specialized things are inherently inaccurate due to the big difference playstyles and opinions make.
|
||||
// See Taiko feedback thread.
|
||||
timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
|
||||
double decay = Math.Pow(DECAY_BASE, timeElapsed / 1000);
|
||||
|
||||
double addition = 1;
|
||||
|
||||
// Only if we are no slider or spinner we get an extra addition
|
||||
if (previousHitObject.BaseHitObject is Hit && BaseHitObject is Hit
|
||||
&& BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime < 1000) // And we only want to check out hitobjects which aren't so far in the past
|
||||
{
|
||||
addition += typeChangeAddition(previousHitObject);
|
||||
addition += rhythmChangeAddition(previousHitObject);
|
||||
}
|
||||
|
||||
double additionFactor = 1.0;
|
||||
// Scale AdditionFactor linearly from 0.4 to 1 for TimeElapsed from 0 to 50
|
||||
if (timeElapsed < 50.0)
|
||||
additionFactor = 0.4 + 0.6 * timeElapsed / 50.0;
|
||||
|
||||
Strain = previousHitObject.Strain * decay + addition * additionFactor;
|
||||
}
|
||||
|
||||
private TypeSwitch lastTypeSwitchEven = TypeSwitch.None;
|
||||
private double typeChangeAddition(TaikoHitObjectDifficulty previousHitObject)
|
||||
{
|
||||
// If we don't have the same hit type, trigger a type change!
|
||||
if (previousHitObject.isRim != isRim)
|
||||
{
|
||||
lastTypeSwitchEven = previousHitObject.sameTypeSince % 2 == 0 ? TypeSwitch.Even : TypeSwitch.Odd;
|
||||
|
||||
// We only want a bonus if the parity of the type switch changes!
|
||||
switch (previousHitObject.lastTypeSwitchEven)
|
||||
{
|
||||
case TypeSwitch.Even:
|
||||
if (lastTypeSwitchEven == TypeSwitch.Odd)
|
||||
return type_change_bonus;
|
||||
break;
|
||||
case TypeSwitch.Odd:
|
||||
if (lastTypeSwitchEven == TypeSwitch.Even)
|
||||
return type_change_bonus;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// No type change? Increment counter and keep track of last type switch
|
||||
else
|
||||
{
|
||||
lastTypeSwitchEven = previousHitObject.lastTypeSwitchEven;
|
||||
sameTypeSince = previousHitObject.sameTypeSince + 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private double rhythmChangeAddition(TaikoHitObjectDifficulty previousHitObject)
|
||||
{
|
||||
// We don't want a division by zero if some random mapper decides to put 2 HitObjects at the same time.
|
||||
if (timeElapsed == 0 || previousHitObject.timeElapsed == 0)
|
||||
return 0;
|
||||
|
||||
double timeElapsedRatio = Math.Max(previousHitObject.timeElapsed / timeElapsed, timeElapsed / previousHitObject.timeElapsed);
|
||||
|
||||
if (timeElapsedRatio >= 8)
|
||||
return 0;
|
||||
|
||||
double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0;
|
||||
|
||||
if (isWithinChangeThreshold(difference))
|
||||
return rhythm_change_bonus;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private bool isWithinChangeThreshold(double value)
|
||||
{
|
||||
return value > rhythm_change_base_threshold && value < 1 - rhythm_change_base_threshold;
|
||||
}
|
||||
|
||||
private enum TypeSwitch
|
||||
{
|
||||
None,
|
||||
Even,
|
||||
Odd
|
||||
}
|
||||
}
|
||||
}
|
@ -6,18 +6,133 @@ using osu.Game.Rulesets.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
public class TaikoDifficultyCalculator : DifficultyCalculator<TaikoHitObject>
|
||||
internal class TaikoDifficultyCalculator : DifficultyCalculator<TaikoHitObject>
|
||||
{
|
||||
public TaikoDifficultyCalculator(Beatmap beatmap) : base(beatmap)
|
||||
private const double star_scaling_factor = 0.04125;
|
||||
|
||||
/// <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>
|
||||
private const double strain_step = 400;
|
||||
|
||||
/// <summary>
|
||||
/// The weighting of each strain value decays to this number * it's previous value
|
||||
/// </summary>
|
||||
private const double decay_weight = 0.9;
|
||||
|
||||
/// <summary>
|
||||
/// HitObjects are stored as a member variable.
|
||||
/// </summary>
|
||||
private readonly List<TaikoHitObjectDifficulty> difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
|
||||
|
||||
public TaikoDifficultyCalculator(Beatmap beatmap)
|
||||
: base(beatmap)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double CalculateInternal(Dictionary<string, string> categoryDifficulty)
|
||||
{
|
||||
return 0;
|
||||
// Fill our custom DifficultyHitObject class, that carries additional information
|
||||
difficultyHitObjects.Clear();
|
||||
|
||||
foreach (var hitObject in Objects)
|
||||
difficultyHitObjects.Add(new TaikoHitObjectDifficulty(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 starRating = calculateDifficulty() * star_scaling_factor;
|
||||
|
||||
if (categoryDifficulty != null)
|
||||
{
|
||||
categoryDifficulty.Add("Strain", starRating.ToString("0.00", CultureInfo.InvariantCulture));
|
||||
categoryDifficulty.Add("Hit window 300", (35 /*HitObjectManager.HitWindow300*/ / TimeRate).ToString("0.00", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return starRating;
|
||||
}
|
||||
|
||||
private bool calculateStrainValues()
|
||||
{
|
||||
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
|
||||
using (List<TaikoHitObjectDifficulty>.Enumerator hitObjectsEnumerator = difficultyHitObjects.GetEnumerator())
|
||||
{
|
||||
if (!hitObjectsEnumerator.MoveNext()) return false;
|
||||
|
||||
TaikoHitObjectDifficulty current = hitObjectsEnumerator.Current;
|
||||
|
||||
// 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())
|
||||
{
|
||||
var next = hitObjectsEnumerator.Current;
|
||||
next?.CalculateStrains(current, TimeRate);
|
||||
current = next;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private double calculateDifficulty()
|
||||
{
|
||||
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
|
||||
|
||||
TaikoHitObjectDifficulty previousHitObject = null;
|
||||
foreach (var 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(TaikoHitObjectDifficulty.DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
|
||||
maximumStrain = previousHitObject.Strain * decay;
|
||||
}
|
||||
|
||||
// Go to the next time interval
|
||||
intervalEndTime += actualStrainStep;
|
||||
}
|
||||
|
||||
// Obtain maximum strain
|
||||
maximumStrain = Math.Max(hitObject.Strain, 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;
|
||||
}
|
||||
|
||||
protected override BeatmapConverter<TaikoHitObject> CreateBeatmapConverter() => new TaikoBeatmapConverter();
|
||||
|
@ -81,6 +81,7 @@
|
||||
<Compile Include="Replays\TaikoFramedReplayInputHandler.cs" />
|
||||
<Compile Include="Replays\TaikoAutoReplay.cs" />
|
||||
<Compile Include="Objects\TaikoHitObject.cs" />
|
||||
<Compile Include="Objects\TaikoHitObjectDifficulty.cs" />
|
||||
<Compile Include="TaikoDifficultyCalculator.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Scoring\TaikoScoreProcessor.cs" />
|
||||
|
19
osu.Game/Audio/SampleInfoList.cs
Normal file
19
osu.Game/Audio/SampleInfoList.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Audio
|
||||
{
|
||||
public class SampleInfoList : List<SampleInfo>
|
||||
{
|
||||
public SampleInfoList()
|
||||
{
|
||||
}
|
||||
|
||||
public SampleInfoList(IEnumerable<SampleInfo> elements)
|
||||
{
|
||||
AddRange(elements);
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,10 @@ namespace osu.Game.Beatmaps
|
||||
protected DifficultyCalculator(Beatmap beatmap)
|
||||
{
|
||||
Objects = CreateBeatmapConverter().Convert(beatmap).HitObjects;
|
||||
|
||||
foreach (var h in Objects)
|
||||
h.ApplyDefaults(beatmap.TimingInfo, beatmap.BeatmapInfo.Difficulty);
|
||||
|
||||
PreprocessHitObjects();
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,6 @@ using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
@ -29,7 +28,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// and can be treated as the default samples for the hit object.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public List<SampleInfo> Samples = new List<SampleInfo>();
|
||||
public SampleInfoList Samples = new SampleInfoList();
|
||||
|
||||
/// <summary>
|
||||
/// Applies default values to this HitObject.
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
};
|
||||
}
|
||||
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
|
||||
{
|
||||
return new ConvertSlider
|
||||
{
|
||||
|
@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
}
|
||||
|
||||
// Generate the final per-node samples
|
||||
var nodeSamples = new List<List<SampleInfo>>(nodes);
|
||||
var nodeSamples = new List<SampleInfoList>(nodes);
|
||||
for (int i = 0; i <= repeatCount; i++)
|
||||
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
|
||||
|
||||
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
/// <param name="repeatCount">The slider repeat count.</param>
|
||||
/// <param name="repeatSamples">The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider.</param>
|
||||
/// <returns>The hit object.</returns>
|
||||
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples);
|
||||
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a legacy Spinner-type hit object.
|
||||
@ -214,9 +214,9 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
/// <returns>The hit object.</returns>
|
||||
protected abstract HitObject CreateSpinner(Vector2 position, double endTime);
|
||||
|
||||
private List<SampleInfo> convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
|
||||
private SampleInfoList convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
|
||||
{
|
||||
var soundTypes = new List<SampleInfo>
|
||||
var soundTypes = new SampleInfoList
|
||||
{
|
||||
new SampleInfo
|
||||
{
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
public CurveType CurveType { get; set; }
|
||||
public double Distance { get; set; }
|
||||
|
||||
public List<List<SampleInfo>> RepeatSamples { get; set; }
|
||||
public List<SampleInfoList> RepeatSamples { get; set; }
|
||||
public int RepeatCount { get; set; } = 1;
|
||||
|
||||
public double EndTime { get; set; }
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
||||
};
|
||||
}
|
||||
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
|
||||
{
|
||||
return new ConvertSlider
|
||||
{
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||
};
|
||||
}
|
||||
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
|
||||
{
|
||||
return new ConvertSlider
|
||||
{
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
||||
};
|
||||
}
|
||||
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
|
||||
{
|
||||
return new ConvertSlider
|
||||
{
|
||||
|
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// <summary>
|
||||
/// The samples to be played when each repeat node is hit (0 -> first repeat node, 1 -> second repeat node, etc).
|
||||
/// </summary>
|
||||
List<List<SampleInfo>> RepeatSamples { get; }
|
||||
List<SampleInfoList> RepeatSamples { get; }
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Audio\SampleInfo.cs" />
|
||||
<Compile Include="Audio\SampleInfoList.cs" />
|
||||
<Compile Include="Beatmaps\Drawables\BeatmapBackgroundSprite.cs" />
|
||||
<Compile Include="Beatmaps\DifficultyCalculator.cs" />
|
||||
<Compile Include="Online\API\Requests\PostMessageRequest.cs" />
|
||||
|
Loading…
Reference in New Issue
Block a user