1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 04:47:24 +08:00

Merge branch 'master' into fix-early-break-cull

This commit is contained in:
Bartłomiej Dach 2020-10-13 18:02:08 +02:00 committed by GitHub
commit 1af17fbd5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 790 additions and 320 deletions

View File

@ -5,24 +5,24 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game;
using osu.Game.Configuration;
namespace osu.Desktop.Windows
{
public class GameplayWinKeyBlocker : Component
{
private Bindable<bool> allowScreenSuspension;
private Bindable<bool> disableWinKey;
private Bindable<bool> localUserPlaying;
private GameHost host;
[Resolved]
private GameHost host { get; set; }
[BackgroundDependencyLoader]
private void load(GameHost host, OsuConfigManager config)
private void load(OsuGame game, OsuConfigManager config)
{
this.host = host;
allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
allowScreenSuspension.BindValueChanged(_ => updateBlocking());
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
@ -30,7 +30,7 @@ namespace osu.Desktop.Windows
private void updateBlocking()
{
bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value;
bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@ -56,6 +57,7 @@ namespace osu.Game.Rulesets.Catch.Objects
Volume = s.Volume
}).ToList();
int nodeIndex = 0;
SliderEventDescriptor? lastEvent = null;
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Objects
case SliderEventType.Repeat:
AddNested(new Fruit
{
Samples = Samples,
Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X,
});
@ -119,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration
{
get => this.SpanCount() * Path.Distance / Velocity;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
set => throw new NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
public double EndTime => StartTime + Duration;

View File

@ -83,11 +83,17 @@ namespace osu.Game.Rulesets.Mania.Tests
RandomZ = snapshot.RandomZ;
}
public override void PostProcess()
{
base.PostProcess();
Objects.Sort();
}
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;
public override bool Equals(ConvertMapping<ConvertValue> other) => base.Equals(other) && Equals(other as ManiaConvertMapping);
}
public struct ConvertValue : IEquatable<ConvertValue>
public struct ConvertValue : IEquatable<ConvertValue>, IComparable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
@ -102,5 +108,15 @@ namespace osu.Game.Rulesets.Mania.Tests
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
&& Column == other.Column;
public int CompareTo(ConvertValue other)
{
var result = StartTime.CompareTo(other.StartTime);
if (result != 0)
return result;
return Column.CompareTo(other.Column);
}
}
}

View File

@ -116,7 +116,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
prevNoteTimes.RemoveAt(0);
prevNoteTimes.Add(newNoteTime);
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
if (prevNoteTimes.Count >= 2)
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
}
private double lastTime;
@ -180,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
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));
computeDensity(endTimeData.EndTime);

View File

@ -3,8 +3,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
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.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@ -25,8 +26,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
private const float osu_base_scoring_distance = 100;
public readonly double EndTime;
public readonly double SegmentDuration;
public readonly int StartTime;
public readonly int EndTime;
public readonly int SegmentDuration;
public readonly int SpanCount;
private PatternType convertType;
@ -41,20 +43,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats;
SpanCount = repeatsData?.SpanCount() ?? 1;
Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
// The true distance, accounting for any repeats
double distance = (distanceData?.Distance ?? 0) * SpanCount;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
double beatLength;
#pragma warning disable 618
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
EndTime = hitObject.StartTime + osuDuration;
SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
SpanCount = repeatsData?.SpanCount() ?? 1;
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()
@ -76,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in originalPattern.HitObjects)
{
if (!Precision.AlmostEquals(EndTime, obj.GetEndTime()))
if (EndTime != (int)Math.Round(obj.GetEndTime()))
intermediatePattern.Add(obj);
else
endTimePattern.Add(obj);
@ -91,35 +99,35 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (TotalColumns == 1)
{
var pattern = new Pattern();
addToPattern(pattern, 0, HitObject.StartTime, EndTime);
addToPattern(pattern, 0, StartTime, EndTime);
return pattern;
}
if (SpanCount > 1)
{
if (SegmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1);
return generateRandomHoldNotes(StartTime, 1);
if (SegmentDuration <= 120)
{
convertType |= PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
return generateRandomNotes(StartTime, SpanCount + 1);
}
if (SegmentDuration <= 160)
return generateStair(HitObject.StartTime);
return generateStair(StartTime);
if (SegmentDuration <= 200 && ConversionDifficulty > 3)
return generateRandomMultipleNotes(HitObject.StartTime);
return generateRandomMultipleNotes(StartTime);
double duration = EndTime - HitObject.StartTime;
double duration = EndTime - StartTime;
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)
return generateTiledHoldNotes(HitObject.StartTime);
return generateTiledHoldNotes(StartTime);
return generateHoldAndNormalNotes(HitObject.StartTime);
return generateHoldAndNormalNotes(StartTime);
}
if (SegmentDuration <= 110)
@ -128,37 +136,37 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
convertType |= PatternType.ForceNotStack;
else
convertType &= ~PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2);
return generateRandomNotes(StartTime, SegmentDuration < 80 ? 1 : 2);
}
if (ConversionDifficulty > 6.5)
{
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 (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 (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))
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>
@ -167,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="startTime">Start time of each hold note.</param>
/// <param name="noteCount">Number of hold notes.</param>
/// <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="noteCount">The number of notes.</param>
/// <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 - - -
@ -234,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateStair(double startTime)
private Pattern generateStair(int startTime)
{
// - - - -
// x - - -
@ -286,7 +294,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomMultipleNotes(double startTime)
private Pattern generateRandomMultipleNotes(int startTime)
{
// - - - -
// 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="p4">The probability required for 4 hold notes to be generated.</param>
/// <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;
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)
p2 = 1;
@ -379,7 +387,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="startTime">The first hold note start time.</param>
/// <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);
// 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);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
@ -401,7 +412,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i < columnRepeat; i++)
{
nextColumn = FindAvailableColumn(nextColumn, pattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
addToPattern(pattern, nextColumn, startTime, endTime);
startTime += SegmentDuration;
}
@ -413,7 +424,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="startTime">The start time of notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateHoldAndNormalNotes(double startTime)
private Pattern generateHoldAndNormalNotes(int startTime)
{
// - - - -
// ■ x x -
@ -448,7 +459,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i <= SpanCount; i++)
{
if (!(ignoreHead && startTime == HitObject.StartTime))
if (!(ignoreHead && startTime == StartTime))
{
for (int j = 0; j < noteCount; j++)
{
@ -471,19 +482,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param>
/// <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>
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
/// </summary>
/// <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))
return null;
// mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
// avoid slicing the list & creating copies, if at all possible.
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="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>
private void addToPattern(Pattern pattern, int column, double startTime, double endTime)
private void addToPattern(Pattern pattern, int column, int startTime, int endTime)
{
ManiaHitObject newObject;

View File

@ -14,12 +14,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
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)
: base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap 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()
@ -40,18 +45,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
break;
case 8:
addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
addToPattern(pattern, getRandomColumn(), generateHold);
break;
default:
if (TotalColumns > 0)
addToPattern(pattern, GetRandomColumn(), generateHold);
addToPattern(pattern, getRandomColumn(0), generateHold);
break;
}
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>
/// Constructs and adds a note to a pattern.
/// </summary>

View File

@ -397,7 +397,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 4:
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;
break;
@ -408,11 +412,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 6:
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;
}
// 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();
int noteCount = GetRandomNoteCount(p2, p3);

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit
int minColumn = int.MaxValue;
int maxColumn = int.MinValue;
foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>())
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
{
if (obj.Column < minColumn)
minColumn = obj.Column;
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>())
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
obj.Column += columnDelta;
}
}

View File

@ -10,7 +10,7 @@
["soft-hitnormal"],
["drum-hitnormal"]
],
"Samples": ["drum-hitnormal"]
"Samples": ["-hitnormal"]
}, {
"StartTime": 1875.0,
"EndTime": 2750.0,
@ -19,7 +19,7 @@
["soft-hitnormal"],
["drum-hitnormal"]
],
"Samples": ["drum-hitnormal"]
"Samples": ["-hitnormal"]
}]
}, {
"StartTime": 3750.0,

View File

@ -21,6 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
InternalChild = circlePiece = new HitCirclePiece();
}
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override void Update()
{
base.Update();

View File

@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.UpdateHitObject(HitObject);
editorBeatmap?.Update(HitObject);
}
public override MenuItem[] ContextMenuItems => new MenuItem[]

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.OnSelectionChanged();
bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider);
bool canOperate = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
SelectionBox.CanRotate = canOperate;
SelectionBox.CanScaleX = canOperate;
@ -232,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// <param name="points">The points to calculate a quad for.</param>
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
{
if (!SelectedHitObjects.Any())
if (!EditorBeatmap.SelectedHitObjects.Any())
return new Quad();
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
@ -253,10 +253,10 @@ namespace osu.Game.Rulesets.Osu.Edit
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
private OsuHitObject[] selectedMovableObjects => SelectedHitObjects
.OfType<OsuHitObject>()
.Where(h => !(h is Spinner))
.ToArray();
private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects
.OfType<OsuHitObject>()
.Where(h => !(h is Spinner))
.ToArray();
/// <summary>
/// Rotate a point around an arbitrary origin.

View File

@ -137,6 +137,10 @@ namespace osu.Game.Rulesets.Osu.Objects
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
// For now, the samples are attached to and played by the slider itself at the correct end time.
Samples = this.GetNodeSamples(repeatCount + 1);
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@ -230,15 +234,12 @@ namespace osu.Game.Rulesets.Osu.Objects
tick.Samples = sampleList;
foreach (var repeat in NestedHitObjects.OfType<SliderRepeat>())
repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1);
repeat.Samples = this.GetNodeSamples(repeat.RepeatIndex + 1);
if (HeadCircle != null)
HeadCircle.Samples = getNodeSamples(0);
HeadCircle.Samples = this.GetNodeSamples(0);
}
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;

View File

@ -52,32 +52,32 @@ namespace osu.Game.Rulesets.Taiko.Edit
public void SetStrongState(bool state)
{
var hits = SelectedHitObjects.OfType<Hit>();
var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
ChangeHandler.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in hits)
{
if (h.IsStrong != state)
{
h.IsStrong = state;
EditorBeatmap.UpdateHitObject(h);
EditorBeatmap.Update(h);
}
}
ChangeHandler.EndChange();
EditorBeatmap.EndChange();
}
public void SetRimState(bool state)
{
var hits = SelectedHitObjects.OfType<Hit>();
var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
ChangeHandler.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in hits)
h.Type = state ? HitType.Rim : HitType.Centre;
ChangeHandler.EndChange();
EditorBeatmap.EndChange();
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
@ -95,8 +95,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
base.UpdateTernaryStates();
selectionRimState.Value = GetStateFromSelection(SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
selectionStrongState.Value = GetStateFromSelection(SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
selectionRimState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
}
}
}

View File

@ -28,5 +28,28 @@ namespace osu.Game.Tests.Beatmaps
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);
}
}
}

View File

@ -651,5 +651,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
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));
}
}
}
}

View File

@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editing
{
[TestFixture]
public class TransactionalCommitComponentTest
{
private TestHandler handler;
[SetUp]
public void SetUp()
{
handler = new TestHandler();
}
[Test]
public void TestCommitTransaction()
{
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.BeginChange();
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.EndChange();
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
}
[Test]
public void TestSaveOutsideOfTransactionTriggersUpdates()
{
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.SaveState();
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
handler.SaveState();
Assert.That(handler.StateUpdateCount, Is.EqualTo(2));
}
[Test]
public void TestEventsFire()
{
int transactionBegan = 0;
int transactionEnded = 0;
int stateSaved = 0;
handler.TransactionBegan += () => transactionBegan++;
handler.TransactionEnded += () => transactionEnded++;
handler.SaveStateTriggered += () => stateSaved++;
handler.BeginChange();
Assert.That(transactionBegan, Is.EqualTo(1));
handler.EndChange();
Assert.That(transactionEnded, Is.EqualTo(1));
Assert.That(stateSaved, Is.EqualTo(0));
handler.SaveState();
Assert.That(stateSaved, Is.EqualTo(1));
}
[Test]
public void TestSaveDuringTransactionDoesntTriggerUpdate()
{
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.BeginChange();
handler.SaveState();
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.EndChange();
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
}
[Test]
public void TestEndWithoutBeginThrows()
{
handler.BeginChange();
handler.EndChange();
Assert.That(() => handler.EndChange(), Throws.TypeOf<InvalidOperationException>());
}
private class TestHandler : TransactionalCommitComponent
{
public int StateUpdateCount { get; private set; }
protected override void UpdateState()
{
StateUpdateCount++;
}
}
}
}

View File

@ -197,5 +197,22 @@ namespace osu.Game.Tests.NonVisual.Filtering
carouselItem.Filter(criteria);
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);
}
}
}

View 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

View File

@ -13,9 +13,12 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Lists;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Beatmaps
{
@ -124,13 +127,22 @@ namespace osu.Game.Beatmaps
/// <returns>The <see cref="DifficultyRating"/> that best describes <paramref name="starRating"/>.</returns>
public static DifficultyRating GetDifficultyRating(double starRating)
{
if (starRating < 2.0) return DifficultyRating.Easy;
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;
if (Precision.AlmostBigger(starRating, 6.5, 0.005))
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;
@ -228,6 +240,24 @@ namespace osu.Game.Beatmaps
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
{
return difficultyCache[key] = new StarDifficulty();

View File

@ -70,53 +70,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
else if (type.HasFlag(LegacyHitObjectType.Slider))
{
PathType pathType = PathType.Catmull;
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]);
if (repeatCount > 9000)
@ -183,10 +138,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples);
// The samples are played when the slider ends, which is the last node
result.Samples = nodeSamples[^1];
result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples);
}
else if (type.HasFlag(LegacyHitObjectType.Spinner))
{
@ -255,8 +207,108 @@ namespace osu.Game.Rulesets.Objects.Legacy
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 (vertices.Length != 3)
@ -268,29 +320,64 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
}
var points = new List<PathControlPoint>(vertices.Length)
{
new PathControlPoint
{
Position = { Value = vertices[0] },
Type = { Value = type }
}
};
// The first control point must have a definite type.
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])
{
points[^1].Type.Value = type;
if (vertices[endIndex].Position.Value != vertices[endIndex - 1].Position.Value)
continue;
}
points.Add(new PathControlPoint { Position = { Value = vertices[i] } });
// 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;
}
return points.ToArray();
if (endIndex > startIndex)
yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex);
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));
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 } };
}
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));
}
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>

View File

@ -35,5 +35,15 @@ namespace osu.Game.Rulesets.Objects.Types
/// </summary>
/// <param name="obj">The object that has repeats.</param>
public static int SpanCount(this IHasRepeats obj) => obj.RepeatCount + 1;
/// <summary>
/// Retrieves the samples at a particular node in a <see cref="IHasRepeats"/> object.
/// </summary>
/// <param name="obj">The <see cref="HitObject"/>.</param>
/// <param name="nodeIndex">The node to attempt to retrieve the samples at.</param>
/// <returns>The samples at the given node index, or <paramref name="obj"/>'s default samples if the given node doesn't exist.</returns>
public static IList<HitSampleInfo> GetNodeSamples<T>(this T obj, int nodeIndex)
where T : HitObject, IHasRepeats
=> nodeIndex < obj.NodeSamples.Count ? obj.NodeSamples[nodeIndex] : obj.Samples;
}
}

View File

@ -203,7 +203,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
// handle positional change etc.
foreach (var obj in selectedHitObjects)
Beatmap.UpdateHitObject(obj);
Beatmap.Update(obj);
changeHandler?.EndChange();
isDraggingBlueprint = false;
@ -436,8 +436,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
// Apply the start time at the newly snapped-to position
double offset = result.Time.Value - draggedObject.StartTime;
foreach (HitObject obj in SelectionHandler.SelectedHitObjects)
foreach (HitObject obj in Beatmap.SelectedHitObjects)
{
obj.StartTime += offset;
Beatmap.Update(obj);
}
}
return true;

View File

@ -37,15 +37,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
public int SelectedCount => selectedBlueprints.Count;
public IEnumerable<HitObject> SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject);
private Drawable content;
private OsuSpriteText selectionDetailsText;
protected SelectionBox SelectionBox { get; private set; }
[Resolved(CanBeNull = true)]
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved(CanBeNull = true)]
@ -245,9 +243,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void deleteSelected()
{
ChangeHandler?.BeginChange();
EditorBeatmap?.RemoveRange(selectedBlueprints.Select(b => b.HitObject));
ChangeHandler?.EndChange();
EditorBeatmap.RemoveRange(selectedBlueprints.Select(b => b.HitObject));
}
#endregion
@ -314,9 +310,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName)
{
ChangeHandler?.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in SelectedHitObjects)
foreach (var h in EditorBeatmap.SelectedHitObjects)
{
// Make sure there isn't already an existing sample
if (h.Samples.Any(s => s.Name == sampleName))
@ -325,7 +321,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
h.Samples.Add(new HitSampleInfo { Name = sampleName });
}
ChangeHandler?.EndChange();
EditorBeatmap.EndChange();
}
/// <summary>
@ -335,19 +331,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception>
public void SetNewCombo(bool state)
{
ChangeHandler?.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in SelectedHitObjects)
foreach (var h in EditorBeatmap.SelectedHitObjects)
{
var comboInfo = h as IHasComboInformation;
if (comboInfo == null || comboInfo.NewCombo == state) continue;
comboInfo.NewCombo = state;
EditorBeatmap?.UpdateHitObject(h);
EditorBeatmap.Update(h);
}
ChangeHandler?.EndChange();
EditorBeatmap.EndChange();
}
/// <summary>
@ -356,12 +352,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName)
{
ChangeHandler?.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in SelectedHitObjects)
foreach (var h in EditorBeatmap.SelectedHitObjects)
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
ChangeHandler?.EndChange();
EditorBeatmap.EndChange();
}
#endregion
@ -432,11 +428,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
protected virtual void UpdateTernaryStates()
{
SelectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
SelectionNewComboState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
foreach (var (sampleName, bindable) in SelectionSampleStates)
{
bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
bindable.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
}
}

View File

@ -392,6 +392,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return;
repeatHitObject.RepeatCount = proposedCount;
beatmap.Update(hitObject);
break;
case IHasDuration endTimeHitObject:
@ -401,10 +402,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return;
endTimeHitObject.Duration = snappedTime - hitObject.StartTime;
beatmap.Update(hitObject);
break;
}
beatmap.UpdateHitObject(hitObject);
}
}

View File

@ -516,14 +516,14 @@ namespace osu.Game.Screens.Edit
foreach (var h in objects)
h.StartTime += timeOffset;
changeHandler.BeginChange();
editorBeatmap.BeginChange();
editorBeatmap.SelectedHitObjects.Clear();
editorBeatmap.AddRange(objects);
editorBeatmap.SelectedHitObjects.AddRange(objects);
changeHandler.EndChange();
editorBeatmap.EndChange();
}
protected void Undo() => changeHandler.RestoreState(-1);

View File

@ -8,7 +8,6 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
@ -18,7 +17,7 @@ using osu.Game.Skinning;
namespace osu.Game.Screens.Edit
{
public class EditorBeatmap : Component, IBeatmap, IBeatSnapProvider
public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapProvider
{
/// <summary>
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="EditorBeatmap"/>.
@ -89,9 +88,11 @@ namespace osu.Game.Screens.Edit
private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
private readonly HashSet<HitObject> pendingUpdates = new HashSet<HitObject>();
private readonly List<HitObject> batchPendingInserts = new List<HitObject>();
private bool isBatchApplying;
private readonly List<HitObject> batchPendingDeletes = new List<HitObject>();
private readonly HashSet<HitObject> batchPendingUpdates = new HashSet<HitObject>();
/// <summary>
/// Adds a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>.
@ -99,11 +100,10 @@ namespace osu.Game.Screens.Edit
/// <param name="hitObjects">The <see cref="HitObject"/>s to add.</param>
public void AddRange(IEnumerable<HitObject> hitObjects)
{
ApplyBatchChanges(_ =>
{
foreach (var h in hitObjects)
Add(h);
});
BeginChange();
foreach (var h in hitObjects)
Add(h);
EndChange();
}
/// <summary>
@ -131,26 +131,28 @@ namespace osu.Game.Screens.Edit
mutableHitObjects.Insert(index, hitObject);
if (isBatchApplying)
batchPendingInserts.Add(hitObject);
else
{
// must be run after any change to hitobject ordering
beatmapProcessor?.PreProcess();
processHitObject(hitObject);
beatmapProcessor?.PostProcess();
HitObjectAdded?.Invoke(hitObject);
}
BeginChange();
batchPendingInserts.Add(hitObject);
EndChange();
}
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
public void UpdateHitObject([NotNull] HitObject hitObject)
public void Update([NotNull] HitObject hitObject)
{
pendingUpdates.Add(hitObject);
// updates are debounced regardless of whether a batch is active.
batchPendingUpdates.Add(hitObject);
}
/// <summary>
/// Update all hit objects with potentially changed difficulty or control point data.
/// </summary>
public void UpdateAllHitObjects()
{
foreach (var h in HitObjects)
batchPendingUpdates.Add(h);
}
/// <summary>
@ -175,11 +177,10 @@ namespace osu.Game.Screens.Edit
/// <param name="hitObjects">The <see cref="HitObject"/>s to remove.</param>
public void RemoveRange(IEnumerable<HitObject> hitObjects)
{
ApplyBatchChanges(_ =>
{
foreach (var h in hitObjects)
Remove(h);
});
BeginChange();
foreach (var h in hitObjects)
Remove(h);
EndChange();
}
/// <summary>
@ -203,50 +204,45 @@ namespace osu.Game.Screens.Edit
bindable.UnbindAll();
startTimeBindables.Remove(hitObject);
if (isBatchApplying)
batchPendingDeletes.Add(hitObject);
else
{
// must be run after any change to hitobject ordering
beatmapProcessor?.PreProcess();
processHitObject(hitObject);
beatmapProcessor?.PostProcess();
HitObjectRemoved?.Invoke(hitObject);
}
BeginChange();
batchPendingDeletes.Add(hitObject);
EndChange();
}
private readonly List<HitObject> batchPendingInserts = new List<HitObject>();
private readonly List<HitObject> batchPendingDeletes = new List<HitObject>();
/// <summary>
/// Apply a batch of operations in one go, without performing Pre/Postprocessing each time.
/// </summary>
/// <param name="applyFunction">The function which will apply the batch changes.</param>
public void ApplyBatchChanges(Action<EditorBeatmap> applyFunction)
protected override void Update()
{
if (isBatchApplying)
throw new InvalidOperationException("Attempting to perform a batch application from within an existing batch");
base.Update();
isBatchApplying = true;
if (batchPendingUpdates.Count > 0)
UpdateState();
}
applyFunction(this);
protected override void UpdateState()
{
if (batchPendingUpdates.Count == 0 && batchPendingDeletes.Count == 0 && batchPendingInserts.Count == 0)
return;
beatmapProcessor?.PreProcess();
foreach (var h in batchPendingDeletes) processHitObject(h);
foreach (var h in batchPendingInserts) processHitObject(h);
foreach (var h in batchPendingUpdates) processHitObject(h);
beatmapProcessor?.PostProcess();
foreach (var h in batchPendingDeletes) HitObjectRemoved?.Invoke(h);
foreach (var h in batchPendingInserts) HitObjectAdded?.Invoke(h);
// callbacks may modify the lists so let's be safe about it
var deletes = batchPendingDeletes.ToArray();
batchPendingDeletes.Clear();
var inserts = batchPendingInserts.ToArray();
batchPendingInserts.Clear();
isBatchApplying = false;
var updates = batchPendingUpdates.ToArray();
batchPendingUpdates.Clear();
foreach (var h in deletes) HitObjectRemoved?.Invoke(h);
foreach (var h in inserts) HitObjectAdded?.Invoke(h);
foreach (var h in updates) HitObjectUpdated?.Invoke(h);
}
/// <summary>
@ -254,28 +250,6 @@ namespace osu.Game.Screens.Edit
/// </summary>
public void Clear() => RemoveRange(HitObjects.ToArray());
protected override void Update()
{
base.Update();
// debounce updates as they are common and may come from input events, which can run needlessly many times per update frame.
if (pendingUpdates.Count > 0)
{
beatmapProcessor?.PreProcess();
foreach (var hitObject in pendingUpdates)
processHitObject(hitObject);
beatmapProcessor?.PostProcess();
// explicitly needs to be fired after PostProcess
foreach (var hitObject in pendingUpdates)
HitObjectUpdated?.Invoke(hitObject);
pendingUpdates.Clear();
}
}
private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty);
private void trackStartTime(HitObject hitObject)
@ -289,7 +263,7 @@ namespace osu.Game.Screens.Edit
var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime);
mutableHitObjects.Insert(insertionIndex + 1, hitObject);
UpdateHitObject(hitObject);
Update(hitObject);
};
}
@ -315,14 +289,5 @@ namespace osu.Game.Screens.Edit
public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor;
public int BeatDivisor => beatDivisor?.Value ?? 1;
/// <summary>
/// Update all hit objects with potentially changed difficulty or control point data.
/// </summary>
public void UpdateBeatmap()
{
foreach (var h in HitObjects)
pendingUpdates.Add(h);
}
}
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit
/// <summary>
/// Tracks changes to the <see cref="Editor"/>.
/// </summary>
public class EditorChangeHandler : IEditorChangeHandler
public class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler
{
public readonly Bindable<bool> CanUndo = new Bindable<bool>();
public readonly Bindable<bool> CanRedo = new Bindable<bool>();
@ -41,7 +41,6 @@ namespace osu.Game.Screens.Edit
}
private readonly EditorBeatmap editorBeatmap;
private int bulkChangesStarted;
private bool isRestoring;
public const int MAX_SAVED_STATES = 50;
@ -54,9 +53,9 @@ namespace osu.Game.Screens.Edit
{
this.editorBeatmap = editorBeatmap;
editorBeatmap.HitObjectAdded += hitObjectAdded;
editorBeatmap.HitObjectRemoved += hitObjectRemoved;
editorBeatmap.HitObjectUpdated += hitObjectUpdated;
editorBeatmap.TransactionBegan += BeginChange;
editorBeatmap.TransactionEnded += EndChange;
editorBeatmap.SaveStateTriggered += SaveState;
patcher = new LegacyEditorBeatmapPatcher(editorBeatmap);
@ -64,28 +63,8 @@ namespace osu.Game.Screens.Edit
SaveState();
}
private void hitObjectAdded(HitObject obj) => SaveState();
private void hitObjectRemoved(HitObject obj) => SaveState();
private void hitObjectUpdated(HitObject obj) => SaveState();
public void BeginChange() => bulkChangesStarted++;
public void EndChange()
protected override void UpdateState()
{
if (bulkChangesStarted == 0)
throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}.");
if (--bulkChangesStarted == 0)
SaveState();
}
public void SaveState()
{
if (bulkChangesStarted > 0)
return;
if (isRestoring)
return;
@ -120,7 +99,7 @@ namespace osu.Game.Screens.Edit
/// <param name="direction">The direction to restore in. If less than 0, an older state will be used. If greater than 0, a newer state will be used.</param>
public void RestoreState(int direction)
{
if (bulkChangesStarted > 0)
if (TransactionActive)
return;
if (savedStates.Count == 0)

View File

@ -68,19 +68,20 @@ namespace osu.Game.Screens.Edit
toRemove.Sort();
toAdd.Sort();
editorBeatmap.ApplyBatchChanges(eb =>
{
// Apply the changes.
for (int i = toRemove.Count - 1; i >= 0; i--)
eb.RemoveAt(toRemove[i]);
editorBeatmap.BeginChange();
if (toAdd.Count > 0)
{
IBeatmap newBeatmap = readBeatmap(newState);
foreach (var i in toAdd)
eb.Insert(i, newBeatmap.HitObjects[i]);
}
});
// Apply the changes.
for (int i = toRemove.Count - 1; i >= 0; i--)
editorBeatmap.RemoveAt(toRemove[i]);
if (toAdd.Count > 0)
{
IBeatmap newBeatmap = readBeatmap(newState);
foreach (var i in toAdd)
editorBeatmap.Insert(i, newBeatmap.HitObjects[i]);
}
editorBeatmap.EndChange();
}
private string readString(byte[] state) => Encoding.UTF8.GetString(state);

View File

@ -93,7 +93,7 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
editorBeatmap.UpdateBeatmap();
editorBeatmap.UpdateAllHitObjects();
}
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// A component that tracks a batch change, only applying after all active changes are completed.
/// </summary>
public abstract class TransactionalCommitComponent : Component
{
/// <summary>
/// Fires whenever a transaction begins. Will not fire on nested transactions.
/// </summary>
public event Action TransactionBegan;
/// <summary>
/// Fires when the last transaction completes.
/// </summary>
public event Action TransactionEnded;
/// <summary>
/// Fires when <see cref="SaveState"/> is called and results in a non-transactional state save.
/// </summary>
public event Action SaveStateTriggered;
public bool TransactionActive => bulkChangesStarted > 0;
private int bulkChangesStarted;
/// <summary>
/// Signal the beginning of a change.
/// </summary>
public void BeginChange()
{
if (bulkChangesStarted++ == 0)
TransactionBegan?.Invoke();
}
/// <summary>
/// Signal the end of a change.
/// </summary>
/// <exception cref="InvalidOperationException">Throws if <see cref="BeginChange"/> was not first called.</exception>
public void EndChange()
{
if (bulkChangesStarted == 0)
throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}.");
if (--bulkChangesStarted == 0)
{
UpdateState();
TransactionEnded?.Invoke();
}
}
/// <summary>
/// Force an update of the state with no attached transaction.
/// This is a no-op if a transaction is already active. Should generally be used as a safety measure to ensure granular changes are not left outside a transaction.
/// </summary>
public void SaveState()
{
if (bulkChangesStarted > 0)
return;
SaveStateTriggered?.Invoke();
UpdateState();
}
protected abstract void UpdateState();
}
}

View File

@ -89,6 +89,11 @@ namespace osu.Game.Screens.Play
public BreakOverlay BreakOverlay;
/// <summary>
/// Whether the gameplay is currently in a break.
/// </summary>
public readonly IBindable<bool> IsBreakTime = new BindableBool();
private BreakTracker breakTracker;
private SkipOverlay skipOverlay;
@ -226,7 +231,6 @@ namespace osu.Game.Screens.Play
DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState());
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState());
breakTracker.IsBreakTime.BindValueChanged(_ => updateGameplayState());
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
@ -256,7 +260,8 @@ namespace osu.Game.Screens.Play
foreach (var mod in Mods.Value.OfType<IApplicableToHealthProcessor>())
mod.ApplyToHealthProcessor(HealthProcessor);
breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
IsBreakTime.BindTo(breakTracker.IsBreakTime);
IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
}
private Drawable createUnderlayComponents() =>
@ -354,6 +359,7 @@ namespace osu.Game.Screens.Play
private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)
{
updateGameplayState();
updatePauseOnFocusLostState();
HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue;
}

View File

@ -10,9 +10,11 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Screens;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Scoring;
using osu.Game.Screens.Backgrounds;
@ -22,7 +24,7 @@ using osuTK;
namespace osu.Game.Screens.Ranking
{
public abstract class ResultsScreen : OsuScreen
public abstract class ResultsScreen : OsuScreen, IKeyBindingHandler<GlobalAction>
{
protected const float BACKGROUND_BLUR = 20;
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
{
protected override Container<Drawable> Content => content;

View File

@ -58,6 +58,14 @@ namespace osu.Game.Screens.Select.Carousel
foreach (var criteriaTerm in criteria.SearchTerms)
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)

View File

@ -43,6 +43,11 @@ namespace osu.Game.Screens.Select
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
{
get => searchText;
@ -50,6 +55,11 @@ namespace osu.Game.Screens.Select
{
searchText = value;
SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray();
SearchNumber = null;
if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0], out int parsed))
SearchNumber = parsed;
}
}

View File

@ -34,6 +34,12 @@ namespace osu.Game.Tests.Beatmaps
var ourResult = convert(name, mods.Select(m => (Mod)Activator.CreateInstance(m)).ToArray());
var expectedResult = read(name);
foreach (var m in ourResult.Mappings)
m.PostProcess();
foreach (var m in expectedResult.Mappings)
m.PostProcess();
Assert.Multiple(() =>
{
int mappingCounter = 0;
@ -239,6 +245,13 @@ namespace osu.Game.Tests.Beatmaps
set => Objects = value;
}
/// <summary>
/// Invoked after this <see cref="ConvertMapping{TConvertValue}"/> is populated to post-process the contained data.
/// </summary>
public virtual void PostProcess()
{
}
public virtual bool Equals(ConvertMapping<TConvertValue> other) => StartTime == other?.StartTime;
}
}