mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 15:03:10 +08:00
Merge branch 'master' into mod-customisation-absorb-mouse-input
This commit is contained in:
commit
c3f1a30447
@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
prevNoteTimes.RemoveAt(0);
|
prevNoteTimes.RemoveAt(0);
|
||||||
prevNoteTimes.Add(newNoteTime);
|
prevNoteTimes.Add(newNoteTime);
|
||||||
|
|
||||||
|
if (prevNoteTimes.Count >= 2)
|
||||||
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
|
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
|
|
||||||
case IHasDuration endTimeData:
|
case IHasDuration endTimeData:
|
||||||
{
|
{
|
||||||
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
|
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
|
||||||
|
|
||||||
recordNote(endTimeData.EndTime, new Vector2(256, 192));
|
recordNote(endTimeData.EndTime, new Vector2(256, 192));
|
||||||
computeDensity(endTimeData.EndTime);
|
computeDensity(endTimeData.EndTime);
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.MathUtils;
|
using osu.Game.Rulesets.Mania.MathUtils;
|
||||||
@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Beatmaps.Formats;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||||
{
|
{
|
||||||
@ -25,8 +26,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const float osu_base_scoring_distance = 100;
|
private const float osu_base_scoring_distance = 100;
|
||||||
|
|
||||||
public readonly double EndTime;
|
public readonly int StartTime;
|
||||||
public readonly double SegmentDuration;
|
public readonly int EndTime;
|
||||||
|
public readonly int SegmentDuration;
|
||||||
public readonly int SpanCount;
|
public readonly int SpanCount;
|
||||||
|
|
||||||
private PatternType convertType;
|
private PatternType convertType;
|
||||||
@ -41,20 +43,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
var distanceData = hitObject as IHasDistance;
|
var distanceData = hitObject as IHasDistance;
|
||||||
var repeatsData = hitObject as IHasRepeats;
|
var repeatsData = hitObject as IHasRepeats;
|
||||||
|
|
||||||
SpanCount = repeatsData?.SpanCount() ?? 1;
|
Debug.Assert(distanceData != null);
|
||||||
|
|
||||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
||||||
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
|
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
|
||||||
|
|
||||||
// The true distance, accounting for any repeats
|
double beatLength;
|
||||||
double distance = (distanceData?.Distance ?? 0) * SpanCount;
|
#pragma warning disable 618
|
||||||
// The velocity of the osu! hit object - calculated as the velocity of a slider
|
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
|
||||||
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
|
#pragma warning restore 618
|
||||||
// The duration of the osu! hit object
|
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
|
||||||
double osuDuration = distance / osuVelocity;
|
else
|
||||||
|
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
|
||||||
|
|
||||||
EndTime = hitObject.StartTime + osuDuration;
|
SpanCount = repeatsData?.SpanCount() ?? 1;
|
||||||
SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
|
StartTime = (int)Math.Round(hitObject.StartTime);
|
||||||
|
|
||||||
|
// This matches stable's calculation.
|
||||||
|
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
|
||||||
|
|
||||||
|
SegmentDuration = (EndTime - StartTime) / SpanCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<Pattern> Generate()
|
public override IEnumerable<Pattern> Generate()
|
||||||
@ -76,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
foreach (var obj in originalPattern.HitObjects)
|
foreach (var obj in originalPattern.HitObjects)
|
||||||
{
|
{
|
||||||
if (!Precision.AlmostEquals(EndTime, obj.GetEndTime()))
|
if (EndTime != (int)Math.Round(obj.GetEndTime()))
|
||||||
intermediatePattern.Add(obj);
|
intermediatePattern.Add(obj);
|
||||||
else
|
else
|
||||||
endTimePattern.Add(obj);
|
endTimePattern.Add(obj);
|
||||||
@ -91,35 +99,35 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
if (TotalColumns == 1)
|
if (TotalColumns == 1)
|
||||||
{
|
{
|
||||||
var pattern = new Pattern();
|
var pattern = new Pattern();
|
||||||
addToPattern(pattern, 0, HitObject.StartTime, EndTime);
|
addToPattern(pattern, 0, StartTime, EndTime);
|
||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SpanCount > 1)
|
if (SpanCount > 1)
|
||||||
{
|
{
|
||||||
if (SegmentDuration <= 90)
|
if (SegmentDuration <= 90)
|
||||||
return generateRandomHoldNotes(HitObject.StartTime, 1);
|
return generateRandomHoldNotes(StartTime, 1);
|
||||||
|
|
||||||
if (SegmentDuration <= 120)
|
if (SegmentDuration <= 120)
|
||||||
{
|
{
|
||||||
convertType |= PatternType.ForceNotStack;
|
convertType |= PatternType.ForceNotStack;
|
||||||
return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
|
return generateRandomNotes(StartTime, SpanCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SegmentDuration <= 160)
|
if (SegmentDuration <= 160)
|
||||||
return generateStair(HitObject.StartTime);
|
return generateStair(StartTime);
|
||||||
|
|
||||||
if (SegmentDuration <= 200 && ConversionDifficulty > 3)
|
if (SegmentDuration <= 200 && ConversionDifficulty > 3)
|
||||||
return generateRandomMultipleNotes(HitObject.StartTime);
|
return generateRandomMultipleNotes(StartTime);
|
||||||
|
|
||||||
double duration = EndTime - HitObject.StartTime;
|
double duration = EndTime - StartTime;
|
||||||
if (duration >= 4000)
|
if (duration >= 4000)
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0);
|
return generateNRandomNotes(StartTime, 0.23, 0, 0);
|
||||||
|
|
||||||
if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
|
if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
|
||||||
return generateTiledHoldNotes(HitObject.StartTime);
|
return generateTiledHoldNotes(StartTime);
|
||||||
|
|
||||||
return generateHoldAndNormalNotes(HitObject.StartTime);
|
return generateHoldAndNormalNotes(StartTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SegmentDuration <= 110)
|
if (SegmentDuration <= 110)
|
||||||
@ -128,37 +136,37 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
convertType |= PatternType.ForceNotStack;
|
convertType |= PatternType.ForceNotStack;
|
||||||
else
|
else
|
||||||
convertType &= ~PatternType.ForceNotStack;
|
convertType &= ~PatternType.ForceNotStack;
|
||||||
return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2);
|
return generateRandomNotes(StartTime, SegmentDuration < 80 ? 1 : 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConversionDifficulty > 6.5)
|
if (ConversionDifficulty > 6.5)
|
||||||
{
|
{
|
||||||
if (convertType.HasFlag(PatternType.LowProbability))
|
if (convertType.HasFlag(PatternType.LowProbability))
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0);
|
return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
|
||||||
|
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03);
|
return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConversionDifficulty > 4)
|
if (ConversionDifficulty > 4)
|
||||||
{
|
{
|
||||||
if (convertType.HasFlag(PatternType.LowProbability))
|
if (convertType.HasFlag(PatternType.LowProbability))
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0);
|
return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
|
||||||
|
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0);
|
return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ConversionDifficulty > 2.5)
|
if (ConversionDifficulty > 2.5)
|
||||||
{
|
{
|
||||||
if (convertType.HasFlag(PatternType.LowProbability))
|
if (convertType.HasFlag(PatternType.LowProbability))
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0);
|
return generateNRandomNotes(StartTime, 0.3, 0, 0);
|
||||||
|
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0);
|
return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convertType.HasFlag(PatternType.LowProbability))
|
if (convertType.HasFlag(PatternType.LowProbability))
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0);
|
return generateNRandomNotes(StartTime, 0.17, 0, 0);
|
||||||
|
|
||||||
return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0);
|
return generateNRandomNotes(StartTime, 0.27, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -167,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// <param name="startTime">Start time of each hold note.</param>
|
/// <param name="startTime">Start time of each hold note.</param>
|
||||||
/// <param name="noteCount">Number of hold notes.</param>
|
/// <param name="noteCount">Number of hold notes.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateRandomHoldNotes(double startTime, int noteCount)
|
private Pattern generateRandomHoldNotes(int startTime, int noteCount)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// ■ - ■ ■
|
// ■ - ■ ■
|
||||||
@ -202,7 +210,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// <param name="startTime">The start time.</param>
|
/// <param name="startTime">The start time.</param>
|
||||||
/// <param name="noteCount">The number of notes.</param>
|
/// <param name="noteCount">The number of notes.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateRandomNotes(double startTime, int noteCount)
|
private Pattern generateRandomNotes(int startTime, int noteCount)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// x - - -
|
// x - - -
|
||||||
@ -234,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startTime">The start time.</param>
|
/// <param name="startTime">The start time.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateStair(double startTime)
|
private Pattern generateStair(int startTime)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// x - - -
|
// x - - -
|
||||||
@ -286,7 +294,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startTime">The start time.</param>
|
/// <param name="startTime">The start time.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateRandomMultipleNotes(double startTime)
|
private Pattern generateRandomMultipleNotes(int startTime)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// x - - -
|
// x - - -
|
||||||
@ -329,7 +337,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// <param name="p3">The probability required for 3 hold notes to be generated.</param>
|
/// <param name="p3">The probability required for 3 hold notes to be generated.</param>
|
||||||
/// <param name="p4">The probability required for 4 hold notes to be generated.</param>
|
/// <param name="p4">The probability required for 4 hold notes to be generated.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4)
|
private Pattern generateNRandomNotes(int startTime, double p2, double p3, double p4)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// ■ - ■ ■
|
// ■ - ■ ■
|
||||||
@ -366,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
|
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
|
||||||
|
|
||||||
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
|
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
|
||||||
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);
|
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
|
||||||
|
|
||||||
if (canGenerateTwoNotes)
|
if (canGenerateTwoNotes)
|
||||||
p2 = 1;
|
p2 = 1;
|
||||||
@ -379,7 +387,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startTime">The first hold note start time.</param>
|
/// <param name="startTime">The first hold note start time.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateTiledHoldNotes(double startTime)
|
private Pattern generateTiledHoldNotes(int startTime)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// ■ ■ ■ ■
|
// ■ ■ ■ ■
|
||||||
@ -394,6 +402,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
int columnRepeat = Math.Min(SpanCount, TotalColumns);
|
int columnRepeat = Math.Min(SpanCount, TotalColumns);
|
||||||
|
|
||||||
|
// Due to integer rounding, this is not guaranteed to be the same as EndTime (the class-level variable).
|
||||||
|
int endTime = startTime + SegmentDuration * SpanCount;
|
||||||
|
|
||||||
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
|
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
|
||||||
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
|
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
|
||||||
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
|
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
|
||||||
@ -401,7 +412,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
for (int i = 0; i < columnRepeat; i++)
|
for (int i = 0; i < columnRepeat; i++)
|
||||||
{
|
{
|
||||||
nextColumn = FindAvailableColumn(nextColumn, pattern);
|
nextColumn = FindAvailableColumn(nextColumn, pattern);
|
||||||
addToPattern(pattern, nextColumn, startTime, EndTime);
|
addToPattern(pattern, nextColumn, startTime, endTime);
|
||||||
startTime += SegmentDuration;
|
startTime += SegmentDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,7 +424,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startTime">The start time of notes.</param>
|
/// <param name="startTime">The start time of notes.</param>
|
||||||
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
|
||||||
private Pattern generateHoldAndNormalNotes(double startTime)
|
private Pattern generateHoldAndNormalNotes(int startTime)
|
||||||
{
|
{
|
||||||
// - - - -
|
// - - - -
|
||||||
// ■ x x -
|
// ■ x x -
|
||||||
@ -448,7 +459,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
for (int i = 0; i <= SpanCount; i++)
|
for (int i = 0; i <= SpanCount; i++)
|
||||||
{
|
{
|
||||||
if (!(ignoreHead && startTime == HitObject.StartTime))
|
if (!(ignoreHead && startTime == StartTime))
|
||||||
{
|
{
|
||||||
for (int j = 0; j < noteCount; j++)
|
for (int j = 0; j < noteCount; j++)
|
||||||
{
|
{
|
||||||
@ -471,19 +482,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to retrieve the sample info list from.</param>
|
/// <param name="time">The time to retrieve the sample info list from.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private IList<HitSampleInfo> sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
|
private IList<HitSampleInfo> sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
|
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to retrieve node samples at.</param>
|
/// <param name="time">The time to retrieve node samples at.</param>
|
||||||
private List<IList<HitSampleInfo>> nodeSamplesAt(double time)
|
private List<IList<HitSampleInfo>> nodeSamplesAt(int time)
|
||||||
{
|
{
|
||||||
if (!(HitObject is IHasPathWithRepeats curveData))
|
if (!(HitObject is IHasPathWithRepeats curveData))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
|
var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
|
||||||
var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
|
|
||||||
|
|
||||||
// avoid slicing the list & creating copies, if at all possible.
|
// avoid slicing the list & creating copies, if at all possible.
|
||||||
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
|
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
|
||||||
@ -496,7 +506,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// <param name="column">The column to add the note to.</param>
|
/// <param name="column">The column to add the note to.</param>
|
||||||
/// <param name="startTime">The start time of the note.</param>
|
/// <param name="startTime">The start time of the note.</param>
|
||||||
/// <param name="endTime">The end time of the note (set to <paramref name="startTime"/> for a non-hold note).</param>
|
/// <param name="endTime">The end time of the note (set to <paramref name="startTime"/> for a non-hold note).</param>
|
||||||
private void addToPattern(Pattern pattern, int column, double startTime, double endTime)
|
private void addToPattern(Pattern pattern, int column, int startTime, int endTime)
|
||||||
{
|
{
|
||||||
ManiaHitObject newObject;
|
ManiaHitObject newObject;
|
||||||
|
|
||||||
|
@ -14,12 +14,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
{
|
{
|
||||||
internal class EndTimeObjectPatternGenerator : PatternGenerator
|
internal class EndTimeObjectPatternGenerator : PatternGenerator
|
||||||
{
|
{
|
||||||
private readonly double endTime;
|
private readonly int endTime;
|
||||||
|
private readonly PatternType convertType;
|
||||||
|
|
||||||
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap)
|
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
|
||||||
: base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
|
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
|
||||||
{
|
{
|
||||||
endTime = (HitObject as IHasDuration)?.EndTime ?? 0;
|
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
|
||||||
|
|
||||||
|
convertType = PreviousPattern.ColumnWithObjects == TotalColumns
|
||||||
|
? PatternType.None
|
||||||
|
: PatternType.ForceNotStack;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<Pattern> Generate()
|
public override IEnumerable<Pattern> Generate()
|
||||||
@ -40,18 +45,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 8:
|
case 8:
|
||||||
addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
|
addToPattern(pattern, getRandomColumn(), generateHold);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if (TotalColumns > 0)
|
addToPattern(pattern, getRandomColumn(0), generateHold);
|
||||||
addToPattern(pattern, GetRandomColumn(), generateHold);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pattern;
|
return pattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getRandomColumn(int? lowerBound = null)
|
||||||
|
{
|
||||||
|
if ((convertType & PatternType.ForceNotStack) > 0)
|
||||||
|
return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound, patterns: PreviousPattern);
|
||||||
|
|
||||||
|
return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructs and adds a note to a pattern.
|
/// Constructs and adds a note to a pattern.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -397,7 +397,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
case 4:
|
case 4:
|
||||||
centreProbability = 0;
|
centreProbability = 0;
|
||||||
p2 = Math.Min(p2 * 2, 0.2);
|
|
||||||
|
// Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
|
||||||
|
// But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
|
||||||
|
// so it needs to be converted to from a probability and then back after the multiplication.
|
||||||
|
p2 = 1 - Math.Max((1 - p2) * 2, 0.8);
|
||||||
p3 = 0;
|
p3 = 0;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -408,11 +412,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
|
|
||||||
case 6:
|
case 6:
|
||||||
centreProbability = 0;
|
centreProbability = 0;
|
||||||
p2 = Math.Min(p2 * 2, 0.5);
|
|
||||||
p3 = Math.Min(p3 * 2, 0.15);
|
// Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
|
||||||
|
// But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
|
||||||
|
// so it needs to be converted to from a probability and then back after the multiplication.
|
||||||
|
p2 = 1 - Math.Max((1 - p2) * 2, 0.5);
|
||||||
|
p3 = 1 - Math.Max((1 - p3) * 2, 0.85);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The stable values were allowed to exceed 1, which indicate <0% probability.
|
||||||
|
// These values needs to be clamped otherwise GetRandomNoteCount() will throw an exception.
|
||||||
|
p2 = Math.Clamp(p2, 0, 1);
|
||||||
|
p3 = Math.Clamp(p3, 0, 1);
|
||||||
|
|
||||||
double centreVal = Random.NextDouble();
|
double centreVal = Random.NextDouble();
|
||||||
int noteCount = GetRandomNoteCount(p2, p3);
|
int noteCount = GetRandomNoteCount(p2, p3);
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
typeof(ManiaModKey7),
|
typeof(ManiaModKey7),
|
||||||
typeof(ManiaModKey8),
|
typeof(ManiaModKey8),
|
||||||
typeof(ManiaModKey9),
|
typeof(ManiaModKey9),
|
||||||
|
typeof(ManiaModKey10),
|
||||||
}.Except(new[] { GetType() }).ToArray();
|
}.Except(new[] { GetType() }).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
|||||||
InternalChild = circlePiece = new HitCirclePiece();
|
InternalChild = circlePiece = new HitCirclePiece();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
BeginPlacement();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
@ -28,5 +28,28 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
|
|
||||||
Assert.That(key1, Is.EqualTo(key2));
|
Assert.That(key1, Is.EqualTo(key2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(1.3, DifficultyRating.Easy)]
|
||||||
|
[TestCase(1.993, DifficultyRating.Easy)]
|
||||||
|
[TestCase(1.998, DifficultyRating.Normal)]
|
||||||
|
[TestCase(2.4, DifficultyRating.Normal)]
|
||||||
|
[TestCase(2.693, DifficultyRating.Normal)]
|
||||||
|
[TestCase(2.698, DifficultyRating.Hard)]
|
||||||
|
[TestCase(3.5, DifficultyRating.Hard)]
|
||||||
|
[TestCase(3.993, DifficultyRating.Hard)]
|
||||||
|
[TestCase(3.997, DifficultyRating.Insane)]
|
||||||
|
[TestCase(5.0, DifficultyRating.Insane)]
|
||||||
|
[TestCase(5.292, DifficultyRating.Insane)]
|
||||||
|
[TestCase(5.297, DifficultyRating.Expert)]
|
||||||
|
[TestCase(6.2, DifficultyRating.Expert)]
|
||||||
|
[TestCase(6.493, DifficultyRating.Expert)]
|
||||||
|
[TestCase(6.498, DifficultyRating.ExpertPlus)]
|
||||||
|
[TestCase(8.3, DifficultyRating.ExpertPlus)]
|
||||||
|
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
|
||||||
|
{
|
||||||
|
var actualBracket = BeatmapDifficultyManager.GetDifficultyRating(starRating);
|
||||||
|
|
||||||
|
Assert.AreEqual(expectedBracket, actualBracket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -651,5 +651,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
Assert.IsInstanceOf<LegacyDifficultyCalculatorBeatmapDecoder>(decoder);
|
Assert.IsInstanceOf<LegacyDifficultyCalculatorBeatmapDecoder>(decoder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiSegmentSliders()
|
||||||
|
{
|
||||||
|
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||||
|
|
||||||
|
using (var resStream = TestResources.OpenResource("multi-segment-slider.osu"))
|
||||||
|
using (var stream = new LineBufferedReader(resStream))
|
||||||
|
{
|
||||||
|
var decoded = decoder.Decode(stream);
|
||||||
|
|
||||||
|
// Multi-segment
|
||||||
|
var first = ((IHasPath)decoded.HitObjects[0]).Path;
|
||||||
|
|
||||||
|
Assert.That(first.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||||
|
Assert.That(first.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
|
||||||
|
Assert.That(first.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
|
||||||
|
Assert.That(first.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||||
|
|
||||||
|
Assert.That(first.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
|
||||||
|
Assert.That(first.ControlPoints[2].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||||
|
Assert.That(first.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(68, 15)));
|
||||||
|
Assert.That(first.ControlPoints[3].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(first.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(259, -132)));
|
||||||
|
Assert.That(first.ControlPoints[4].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(first.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(92, -107)));
|
||||||
|
Assert.That(first.ControlPoints[5].Type.Value, Is.EqualTo(null));
|
||||||
|
|
||||||
|
// Single-segment
|
||||||
|
var second = ((IHasPath)decoded.HitObjects[1]).Path;
|
||||||
|
|
||||||
|
Assert.That(second.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||||
|
Assert.That(second.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
|
||||||
|
Assert.That(second.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
|
||||||
|
Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
|
||||||
|
Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null));
|
||||||
|
|
||||||
|
// Implicit multi-segment
|
||||||
|
var third = ((IHasPath)decoded.HitObjects[2]).Path;
|
||||||
|
|
||||||
|
Assert.That(third.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
|
||||||
|
Assert.That(third.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||||
|
Assert.That(third.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(0, 192)));
|
||||||
|
Assert.That(third.ControlPoints[1].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(third.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(224, 192)));
|
||||||
|
Assert.That(third.ControlPoints[2].Type.Value, Is.EqualTo(null));
|
||||||
|
|
||||||
|
Assert.That(third.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(224, 0)));
|
||||||
|
Assert.That(third.ControlPoints[3].Type.Value, Is.EqualTo(PathType.Bezier));
|
||||||
|
Assert.That(third.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(224, -192)));
|
||||||
|
Assert.That(third.ControlPoints[4].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(third.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(480, -192)));
|
||||||
|
Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null));
|
||||||
|
Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0)));
|
||||||
|
Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,52 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
|
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiModFlattening()
|
||||||
|
{
|
||||||
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
|
Assert.AreEqual(4, combinations.Length);
|
||||||
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
|
Assert.IsTrue(combinations[1] is ModA);
|
||||||
|
Assert.IsTrue(combinations[2] is MultiMod);
|
||||||
|
Assert.IsTrue(combinations[3] is MultiMod);
|
||||||
|
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIncompatibleThroughMultiMod()
|
||||||
|
{
|
||||||
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
|
Assert.AreEqual(3, combinations.Length);
|
||||||
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
|
Assert.IsTrue(combinations[1] is ModA);
|
||||||
|
Assert.IsTrue(combinations[2] is MultiMod);
|
||||||
|
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIncompatibleWithSameInstanceViaMultiMod()
|
||||||
|
{
|
||||||
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
|
Assert.AreEqual(3, combinations.Length);
|
||||||
|
Assert.IsTrue(combinations[0] is ModNoMod);
|
||||||
|
Assert.IsTrue(combinations[1] is ModA);
|
||||||
|
Assert.IsTrue(combinations[2] is MultiMod);
|
||||||
|
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
||||||
|
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
|
||||||
|
}
|
||||||
|
|
||||||
private class ModA : Mod
|
private class ModA : Mod
|
||||||
{
|
{
|
||||||
public override string Name => nameof(ModA);
|
public override string Name => nameof(ModA);
|
||||||
@ -112,6 +158,13 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
|
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ModC : Mod
|
||||||
|
{
|
||||||
|
public override string Name => nameof(ModC);
|
||||||
|
public override string Acronym => nameof(ModC);
|
||||||
|
public override double ScoreMultiplier => 1;
|
||||||
|
}
|
||||||
|
|
||||||
private class ModIncompatibleWithA : Mod
|
private class ModIncompatibleWithA : Mod
|
||||||
{
|
{
|
||||||
public override string Name => $"Incompatible With {nameof(ModA)}";
|
public override string Name => $"Incompatible With {nameof(ModA)}";
|
||||||
|
@ -197,5 +197,22 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
carouselItem.Filter(criteria);
|
carouselItem.Filter(criteria);
|
||||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("202010", true)]
|
||||||
|
[TestCase("20201010", false)]
|
||||||
|
[TestCase("153", true)]
|
||||||
|
[TestCase("1535", false)]
|
||||||
|
public void TestCriteriaMatchingBeatmapIDs(string query, bool filtered)
|
||||||
|
{
|
||||||
|
var beatmap = getExampleBeatmap();
|
||||||
|
beatmap.OnlineBeatmapID = 20201010;
|
||||||
|
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = 1535 };
|
||||||
|
|
||||||
|
var criteria = new FilterCriteria { SearchText = query };
|
||||||
|
var carouselItem = new CarouselBeatmap(beatmap);
|
||||||
|
carouselItem.Filter(criteria);
|
||||||
|
|
||||||
|
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
osu.Game.Tests/Resources/multi-segment-slider.osu
Normal file
11
osu.Game.Tests/Resources/multi-segment-slider.osu
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
osu file format v128
|
||||||
|
|
||||||
|
[HitObjects]
|
||||||
|
// Multi-segment
|
||||||
|
63,301,1000,6,0,P|224:57|B|439:298|131:316|322:169|155:194,1,1040,0|0,0:0|0:0,0:0:0:0:
|
||||||
|
|
||||||
|
// Single-segment
|
||||||
|
63,301,2000,6,0,P|224:57|439:298,1,1040,0|0,0:0|0:0,0:0:0:0:
|
||||||
|
|
||||||
|
// Implicit multi-segment
|
||||||
|
32,192,3000,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800
|
@ -97,6 +97,24 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
|
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultiModSettingsUnboundWhenCopied()
|
||||||
|
{
|
||||||
|
MultiMod original = null;
|
||||||
|
MultiMod copy = null;
|
||||||
|
|
||||||
|
AddStep("create mods", () =>
|
||||||
|
{
|
||||||
|
original = new MultiMod(new OsuModDoubleTime());
|
||||||
|
copy = (MultiMod)original.CreateCopy();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2);
|
||||||
|
|
||||||
|
AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value));
|
||||||
|
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCustomisationMenuNoClickthrough()
|
public void TestCustomisationMenuNoClickthrough()
|
||||||
{
|
{
|
||||||
|
@ -13,9 +13,12 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Lists;
|
using osu.Framework.Lists;
|
||||||
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
@ -124,13 +127,22 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <returns>The <see cref="DifficultyRating"/> that best describes <paramref name="starRating"/>.</returns>
|
/// <returns>The <see cref="DifficultyRating"/> that best describes <paramref name="starRating"/>.</returns>
|
||||||
public static DifficultyRating GetDifficultyRating(double starRating)
|
public static DifficultyRating GetDifficultyRating(double starRating)
|
||||||
{
|
{
|
||||||
if (starRating < 2.0) return DifficultyRating.Easy;
|
if (Precision.AlmostBigger(starRating, 6.5, 0.005))
|
||||||
if (starRating < 2.7) return DifficultyRating.Normal;
|
|
||||||
if (starRating < 4.0) return DifficultyRating.Hard;
|
|
||||||
if (starRating < 5.3) return DifficultyRating.Insane;
|
|
||||||
if (starRating < 6.5) return DifficultyRating.Expert;
|
|
||||||
|
|
||||||
return DifficultyRating.ExpertPlus;
|
return DifficultyRating.ExpertPlus;
|
||||||
|
|
||||||
|
if (Precision.AlmostBigger(starRating, 5.3, 0.005))
|
||||||
|
return DifficultyRating.Expert;
|
||||||
|
|
||||||
|
if (Precision.AlmostBigger(starRating, 4.0, 0.005))
|
||||||
|
return DifficultyRating.Insane;
|
||||||
|
|
||||||
|
if (Precision.AlmostBigger(starRating, 2.7, 0.005))
|
||||||
|
return DifficultyRating.Hard;
|
||||||
|
|
||||||
|
if (Precision.AlmostBigger(starRating, 2.0, 0.005))
|
||||||
|
return DifficultyRating.Normal;
|
||||||
|
|
||||||
|
return DifficultyRating.Easy;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CancellationTokenSource trackedUpdateCancellationSource;
|
private CancellationTokenSource trackedUpdateCancellationSource;
|
||||||
@ -228,6 +240,24 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo);
|
return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo);
|
||||||
}
|
}
|
||||||
|
catch (BeatmapInvalidForRulesetException e)
|
||||||
|
{
|
||||||
|
// Conversion has failed for the given ruleset, so return the difficulty in the beatmap's default ruleset.
|
||||||
|
|
||||||
|
// Ensure the beatmap's default ruleset isn't the one already being converted to.
|
||||||
|
// This shouldn't happen as it means something went seriously wrong, but if it does an endless loop should be avoided.
|
||||||
|
if (rulesetInfo.Equals(beatmapInfo.Ruleset))
|
||||||
|
{
|
||||||
|
Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset}).");
|
||||||
|
return difficultyCache[key] = new StarDifficulty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the cache first because this is now a different ruleset than the one previously guarded against.
|
||||||
|
if (tryGetExisting(beatmapInfo, beatmapInfo.Ruleset, Array.Empty<Mod>(), out var existingDefault, out var existingDefaultKey))
|
||||||
|
return existingDefault;
|
||||||
|
|
||||||
|
return computeDifficulty(existingDefaultKey, beatmapInfo, beatmapInfo.Ruleset);
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return difficultyCache[key] = new StarDifficulty();
|
return difficultyCache[key] = new StarDifficulty();
|
||||||
|
@ -307,12 +307,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
double start = getOffsetTime(Parsing.ParseDouble(split[1]));
|
double start = getOffsetTime(Parsing.ParseDouble(split[1]));
|
||||||
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
|
double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])));
|
||||||
|
|
||||||
var breakEvent = new BreakPeriod(start, end);
|
beatmap.Breaks.Add(new BreakPeriod(start, end));
|
||||||
|
|
||||||
if (!breakEvent.HasEffect)
|
|
||||||
return;
|
|
||||||
|
|
||||||
beatmap.Breaks.Add(breakEvent);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Timing
|
|||||||
public double Duration => EndTime - StartTime;
|
public double Duration => EndTime - StartTime;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the break has any effect. Breaks that are too short are culled before they are added to the beatmap.
|
/// Whether the break has any effect.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasEffect => Duration >= MIN_BREAK_DURATION;
|
public bool HasEffect => Duration >= MIN_BREAK_DURATION;
|
||||||
|
|
||||||
|
@ -105,10 +105,11 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Mod[] CreateDifficultyAdjustmentModCombinations()
|
public Mod[] CreateDifficultyAdjustmentModCombinations()
|
||||||
{
|
{
|
||||||
return createDifficultyAdjustmentModCombinations(Array.Empty<Mod>(), DifficultyAdjustmentMods).ToArray();
|
return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty<Mod>()).ToArray();
|
||||||
|
|
||||||
IEnumerable<Mod> createDifficultyAdjustmentModCombinations(IEnumerable<Mod> currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0)
|
static IEnumerable<Mod> createDifficultyAdjustmentModCombinations(ReadOnlyMemory<Mod> remainingMods, IEnumerable<Mod> currentSet, int currentSetCount = 0)
|
||||||
{
|
{
|
||||||
|
// Return the current set.
|
||||||
switch (currentSetCount)
|
switch (currentSetCount)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
@ -128,18 +129,43 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod
|
// Apply the rest of the remaining mods recursively.
|
||||||
// combinations in further recursions, so a moving subset is used to eliminate this effect
|
for (int i = 0; i < remainingMods.Length; i++)
|
||||||
for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++)
|
|
||||||
{
|
{
|
||||||
var adjustmentMod = adjustmentSet[i];
|
var (nextSet, nextCount) = flatten(remainingMods.Span[i]);
|
||||||
if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod))))
|
|
||||||
|
// Check if any mods in the next set are incompatible with any of the current set.
|
||||||
|
if (currentSet.SelectMany(m => m.IncompatibleMods).Any(c => nextSet.Any(c.IsInstanceOfType)))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
foreach (var combo in createDifficultyAdjustmentModCombinations(currentSet.Append(adjustmentMod), adjustmentSet, currentSetCount + 1, i + 1))
|
// Check if any mods in the next set are the same type as the current set. Mods of the exact same type are not incompatible with themselves.
|
||||||
|
if (currentSet.Any(c => nextSet.Any(n => c.GetType() == n.GetType())))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// If all's good, attach the next set to the current set and recurse further.
|
||||||
|
foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(nextSet), currentSetCount + nextCount))
|
||||||
yield return combo;
|
yield return combo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flattens a mod hierarchy (through MultiMod) as an IEnumerable<Mod>
|
||||||
|
static (IEnumerable<Mod> set, int count) flatten(Mod mod)
|
||||||
|
{
|
||||||
|
if (!(mod is MultiMod multi))
|
||||||
|
return (mod.Yield(), 1);
|
||||||
|
|
||||||
|
IEnumerable<Mod> set = Enumerable.Empty<Mod>();
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
foreach (var nested in multi.Mods)
|
||||||
|
{
|
||||||
|
var (nestedSet, nestedCount) = flatten(nested);
|
||||||
|
set = set.Concat(nestedSet);
|
||||||
|
count += nestedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (set, count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -107,6 +107,9 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
{
|
{
|
||||||
foreach (var breakPeriod in Breaks)
|
foreach (var breakPeriod in Breaks)
|
||||||
{
|
{
|
||||||
|
if (!breakPeriod.HasEffect)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
|
if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
|
||||||
|
|
||||||
this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
|
this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
|
||||||
|
@ -6,7 +6,7 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Mods
|
namespace osu.Game.Rulesets.Mods
|
||||||
{
|
{
|
||||||
public class MultiMod : Mod
|
public sealed class MultiMod : Mod
|
||||||
{
|
{
|
||||||
public override string Name => string.Empty;
|
public override string Name => string.Empty;
|
||||||
public override string Acronym => string.Empty;
|
public override string Acronym => string.Empty;
|
||||||
@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
Mods = mods;
|
Mods = mods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray());
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray();
|
public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,53 +70,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
}
|
}
|
||||||
else if (type.HasFlag(LegacyHitObjectType.Slider))
|
else if (type.HasFlag(LegacyHitObjectType.Slider))
|
||||||
{
|
{
|
||||||
PathType pathType = PathType.Catmull;
|
|
||||||
double? length = null;
|
double? length = null;
|
||||||
|
|
||||||
string[] pointSplit = split[5].Split('|');
|
|
||||||
|
|
||||||
int pointCount = 1;
|
|
||||||
|
|
||||||
foreach (var t in pointSplit)
|
|
||||||
{
|
|
||||||
if (t.Length > 1)
|
|
||||||
pointCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var points = new Vector2[pointCount];
|
|
||||||
|
|
||||||
int pointIndex = 1;
|
|
||||||
|
|
||||||
foreach (string t in pointSplit)
|
|
||||||
{
|
|
||||||
if (t.Length == 1)
|
|
||||||
{
|
|
||||||
switch (t)
|
|
||||||
{
|
|
||||||
case @"C":
|
|
||||||
pathType = PathType.Catmull;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case @"B":
|
|
||||||
pathType = PathType.Bezier;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case @"L":
|
|
||||||
pathType = PathType.Linear;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case @"P":
|
|
||||||
pathType = PathType.PerfectCurve;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string[] temp = t.Split(':');
|
|
||||||
points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
int repeatCount = Parsing.ParseInt(split[6]);
|
int repeatCount = Parsing.ParseInt(split[6]);
|
||||||
|
|
||||||
if (repeatCount > 9000)
|
if (repeatCount > 9000)
|
||||||
@ -183,7 +138,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
for (int i = 0; i < nodes; i++)
|
for (int i = 0; i < nodes; i++)
|
||||||
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
|
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
|
||||||
|
|
||||||
result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples);
|
result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples);
|
||||||
}
|
}
|
||||||
else if (type.HasFlag(LegacyHitObjectType.Spinner))
|
else if (type.HasFlag(LegacyHitObjectType.Spinner))
|
||||||
{
|
{
|
||||||
@ -252,8 +207,108 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
bankInfo.Filename = split.Length > 4 ? split[4] : null;
|
bankInfo.Filename = split.Length > 4 ? split[4] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private PathControlPoint[] convertControlPoints(Vector2[] vertices, PathType type)
|
private PathType convertPathType(string input)
|
||||||
{
|
{
|
||||||
|
switch (input[0])
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
case 'C':
|
||||||
|
return PathType.Catmull;
|
||||||
|
|
||||||
|
case 'B':
|
||||||
|
return PathType.Bezier;
|
||||||
|
|
||||||
|
case 'L':
|
||||||
|
return PathType.Linear;
|
||||||
|
|
||||||
|
case 'P':
|
||||||
|
return PathType.PerfectCurve;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a given point string into a set of path control points.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A point string takes the form: X|1:1|2:2|2:2|3:3|Y|1:1|2:2.
|
||||||
|
/// This has three segments:
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>
|
||||||
|
/// <description>X: { (1,1), (2,2) } (implicit segment)</description>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <description>X: { (2,2), (3,3) } (implicit segment)</description>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <description>Y: { (3,3), (1,1), (2, 2) } (explicit segment)</description>
|
||||||
|
/// </item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="pointString">The point string.</param>
|
||||||
|
/// <param name="offset">The positional offset to apply to the control points.</param>
|
||||||
|
/// <returns>All control points in the resultant path.</returns>
|
||||||
|
private PathControlPoint[] convertPathString(string pointString, Vector2 offset)
|
||||||
|
{
|
||||||
|
// This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints().
|
||||||
|
string[] pointSplit = pointString.Split('|');
|
||||||
|
|
||||||
|
var controlPoints = new List<Memory<PathControlPoint>>();
|
||||||
|
int startIndex = 0;
|
||||||
|
int endIndex = 0;
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
while (++endIndex < pointSplit.Length)
|
||||||
|
{
|
||||||
|
// Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of length 1).
|
||||||
|
if (pointSplit[endIndex].Length > 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment.
|
||||||
|
// The start of the next segment is the index after the type descriptor.
|
||||||
|
string endPoint = endIndex < pointSplit.Length - 1 ? pointSplit[endIndex + 1] : null;
|
||||||
|
|
||||||
|
controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), endPoint, first, offset));
|
||||||
|
startIndex = endIndex;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex > startIndex)
|
||||||
|
controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), null, first, offset));
|
||||||
|
|
||||||
|
return mergePointsLists(controlPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a given point list into a set of path segments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="points">The point list.</param>
|
||||||
|
/// <param name="endPoint">Any extra endpoint to consider as part of the points. This will NOT be returned.</param>
|
||||||
|
/// <param name="first">Whether this is the first segment in the set. If <c>true</c> the first of the returned segments will contain a zero point.</param>
|
||||||
|
/// <param name="offset">The positional offset to apply to the control points.</param>
|
||||||
|
/// <returns>The set of points contained by <paramref name="points"/> as one or more segments of the path, prepended by an extra zero point if <paramref name="first"/> is <c>true</c>.</returns>
|
||||||
|
private IEnumerable<Memory<PathControlPoint>> convertPoints(ReadOnlyMemory<string> points, string endPoint, bool first, Vector2 offset)
|
||||||
|
{
|
||||||
|
PathType type = convertPathType(points.Span[0]);
|
||||||
|
|
||||||
|
int readOffset = first ? 1 : 0; // First control point is zero for the first segment.
|
||||||
|
int readablePoints = points.Length - 1; // Total points readable from the base point span.
|
||||||
|
int endPointLength = endPoint != null ? 1 : 0; // Extra length if an endpoint is given that lies outside the base point span.
|
||||||
|
|
||||||
|
var vertices = new PathControlPoint[readOffset + readablePoints + endPointLength];
|
||||||
|
|
||||||
|
// Fill any non-read points.
|
||||||
|
for (int i = 0; i < readOffset; i++)
|
||||||
|
vertices[i] = new PathControlPoint();
|
||||||
|
|
||||||
|
// Parse into control points.
|
||||||
|
for (int i = 1; i < points.Length; i++)
|
||||||
|
readPoint(points.Span[i], offset, out vertices[readOffset + i - 1]);
|
||||||
|
|
||||||
|
// If an endpoint is given, add it to the end.
|
||||||
|
if (endPoint != null)
|
||||||
|
readPoint(endPoint, offset, out vertices[^1]);
|
||||||
|
|
||||||
|
// Edge-case rules (to match stable).
|
||||||
if (type == PathType.PerfectCurve)
|
if (type == PathType.PerfectCurve)
|
||||||
{
|
{
|
||||||
if (vertices.Length != 3)
|
if (vertices.Length != 3)
|
||||||
@ -265,29 +320,64 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var points = new List<PathControlPoint>(vertices.Length)
|
// The first control point must have a definite type.
|
||||||
{
|
vertices[0].Type.Value = type;
|
||||||
new PathControlPoint
|
|
||||||
{
|
|
||||||
Position = { Value = vertices[0] },
|
|
||||||
Type = { Value = type }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (int i = 1; i < vertices.Length; i++)
|
// A path can have multiple implicit segments of the same type if there are two sequential control points with the same position.
|
||||||
|
// To handle such cases, this code may return multiple path segments with the final control point in each segment having a non-null type.
|
||||||
|
// For the point string X|1:1|2:2|2:2|3:3, this code returns the segments:
|
||||||
|
// X: { (1,1), (2, 2) }
|
||||||
|
// X: { (3, 3) }
|
||||||
|
// Note: (2, 2) is not returned in the second segments, as it is implicit in the path.
|
||||||
|
int startIndex = 0;
|
||||||
|
int endIndex = 0;
|
||||||
|
|
||||||
|
while (++endIndex < vertices.Length - endPointLength)
|
||||||
{
|
{
|
||||||
if (vertices[i] == vertices[i - 1])
|
if (vertices[endIndex].Position.Value != vertices[endIndex - 1].Position.Value)
|
||||||
{
|
|
||||||
points[^1].Type.Value = type;
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Force a type on the last point, and return the current control point set as a segment.
|
||||||
|
vertices[endIndex - 1].Type.Value = type;
|
||||||
|
yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex);
|
||||||
|
|
||||||
|
// Skip the current control point - as it's the same as the one that's just been returned.
|
||||||
|
startIndex = endIndex + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
points.Add(new PathControlPoint { Position = { Value = vertices[i] } });
|
if (endIndex > startIndex)
|
||||||
|
yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex);
|
||||||
|
|
||||||
|
static void readPoint(string value, Vector2 startPos, out PathControlPoint point)
|
||||||
|
{
|
||||||
|
string[] vertexSplit = value.Split(':');
|
||||||
|
|
||||||
|
Vector2 pos = new Vector2((int)Parsing.ParseDouble(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)) - startPos;
|
||||||
|
point = new PathControlPoint { Position = { Value = pos } };
|
||||||
}
|
}
|
||||||
|
|
||||||
return points.ToArray();
|
static bool isLinear(PathControlPoint[] p) => Precision.AlmostEquals(0, (p[1].Position.Value.Y - p[0].Position.Value.Y) * (p[2].Position.Value.X - p[0].Position.Value.X)
|
||||||
|
- (p[1].Position.Value.X - p[0].Position.Value.X) * (p[2].Position.Value.Y - p[0].Position.Value.Y));
|
||||||
|
}
|
||||||
|
|
||||||
static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
|
private PathControlPoint[] mergePointsLists(List<Memory<PathControlPoint>> controlPointList)
|
||||||
|
{
|
||||||
|
int totalCount = 0;
|
||||||
|
|
||||||
|
foreach (var arr in controlPointList)
|
||||||
|
totalCount += arr.Length;
|
||||||
|
|
||||||
|
var mergedArray = new PathControlPoint[totalCount];
|
||||||
|
var mergedArrayMemory = mergedArray.AsMemory();
|
||||||
|
int copyIndex = 0;
|
||||||
|
|
||||||
|
foreach (var arr in controlPointList)
|
||||||
|
{
|
||||||
|
arr.CopyTo(mergedArrayMemory.Slice(copyIndex));
|
||||||
|
copyIndex += arr.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -89,6 +89,11 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
public BreakOverlay BreakOverlay;
|
public BreakOverlay BreakOverlay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the gameplay is currently in a break.
|
||||||
|
/// </summary>
|
||||||
|
public readonly IBindable<bool> IsBreakTime = new BindableBool();
|
||||||
|
|
||||||
private BreakTracker breakTracker;
|
private BreakTracker breakTracker;
|
||||||
|
|
||||||
private SkipOverlay skipOverlay;
|
private SkipOverlay skipOverlay;
|
||||||
@ -226,7 +231,6 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState());
|
DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState());
|
||||||
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState());
|
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState());
|
||||||
breakTracker.IsBreakTime.BindValueChanged(_ => updateGameplayState());
|
|
||||||
|
|
||||||
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
|
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
|
||||||
|
|
||||||
@ -256,7 +260,8 @@ namespace osu.Game.Screens.Play
|
|||||||
foreach (var mod in Mods.Value.OfType<IApplicableToHealthProcessor>())
|
foreach (var mod in Mods.Value.OfType<IApplicableToHealthProcessor>())
|
||||||
mod.ApplyToHealthProcessor(HealthProcessor);
|
mod.ApplyToHealthProcessor(HealthProcessor);
|
||||||
|
|
||||||
breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
|
IsBreakTime.BindTo(breakTracker.IsBreakTime);
|
||||||
|
IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable createUnderlayComponents() =>
|
private Drawable createUnderlayComponents() =>
|
||||||
@ -354,6 +359,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)
|
private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)
|
||||||
{
|
{
|
||||||
|
updateGameplayState();
|
||||||
updatePauseOnFocusLostState();
|
updatePauseOnFocusLostState();
|
||||||
HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue;
|
HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue;
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,11 @@ using osu.Framework.Extensions.Color4Extensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Backgrounds;
|
using osu.Game.Screens.Backgrounds;
|
||||||
@ -22,7 +24,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.Ranking
|
namespace osu.Game.Screens.Ranking
|
||||||
{
|
{
|
||||||
public abstract class ResultsScreen : OsuScreen
|
public abstract class ResultsScreen : OsuScreen, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
protected const float BACKGROUND_BLUR = 20;
|
protected const float BACKGROUND_BLUR = 20;
|
||||||
private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y;
|
private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y;
|
||||||
@ -314,6 +316,22 @@ namespace osu.Game.Screens.Ranking
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(GlobalAction action)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case GlobalAction.Select:
|
||||||
|
statisticsPanel.ToggleVisibility();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(GlobalAction action)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
private class VerticalScrollContainer : OsuScrollContainer
|
private class VerticalScrollContainer : OsuScrollContainer
|
||||||
{
|
{
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content => content;
|
||||||
|
@ -58,6 +58,14 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
|
|
||||||
foreach (var criteriaTerm in criteria.SearchTerms)
|
foreach (var criteriaTerm in criteria.SearchTerms)
|
||||||
match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0);
|
match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0);
|
||||||
|
|
||||||
|
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
|
||||||
|
// this should be done after text matching so we can prioritise matching numbers in metadata.
|
||||||
|
if (!match && criteria.SearchNumber.HasValue)
|
||||||
|
{
|
||||||
|
match = (Beatmap.OnlineBeatmapID == criteria.SearchNumber.Value) ||
|
||||||
|
(Beatmap.BeatmapSet?.OnlineBeatmapSetID == criteria.SearchNumber.Value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match)
|
if (match)
|
||||||
|
@ -43,6 +43,11 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private string searchText;
|
private string searchText;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="SearchText"/> as a number (if it can be parsed as one).
|
||||||
|
/// </summary>
|
||||||
|
public int? SearchNumber { get; private set; }
|
||||||
|
|
||||||
public string SearchText
|
public string SearchText
|
||||||
{
|
{
|
||||||
get => searchText;
|
get => searchText;
|
||||||
@ -50,6 +55,11 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
searchText = value;
|
searchText = value;
|
||||||
SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
|
SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
|
||||||
|
|
||||||
|
SearchNumber = null;
|
||||||
|
|
||||||
|
if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0], out int parsed))
|
||||||
|
SearchNumber = parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user