Merge pull request #14578 from peppy/no-more-sample-control-points-info

Move `SampleControlPoint` to legacy class
This commit is contained in:
Dan Balasescu 2021-09-02 18:33:45 +09:00 committed by GitHub
commit f8d2740670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 190 additions and 76 deletions

View File

@ -3,15 +3,12 @@
using System;
using System.IO;
using NUnit.Framework;
using osuTK;
using osuTK.Graphics;
using osu.Game.Tests.Resources;
using System.Linq;
using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO;
using osu.Game.Rulesets.Catch;
@ -19,9 +16,13 @@ using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Beatmaps.Formats
{
@ -166,7 +167,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var controlPoints = beatmap.ControlPointInfo;
var controlPoints = (LegacyControlPointInfo)beatmap.ControlPointInfo;
Assert.AreEqual(4, controlPoints.TimingPoints.Count);
Assert.AreEqual(5, controlPoints.DifficultyPoints.Count);
@ -240,7 +241,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("overlapping-control-points.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var controlPoints = decoder.Decode(stream).ControlPointInfo;
var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(4));
Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3));

View File

@ -14,6 +14,7 @@ using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Catch;
@ -65,6 +66,47 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
}
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStabilityWithNonLegacyControlPoints(string name)
{
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
// we are testing that the transfer of relevant data to hitobjects (from legacy control points) sticks through encode/decode.
// before the encode step, the legacy information is removed here.
decoded.beatmap.ControlPointInfo = removeLegacyControlPointTypes(decoded.beatmap.ControlPointInfo);
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
// in this process, we may lose some detail in the control points section.
// let's focus on only the hitobjects.
var originalHitObjects = decoded.beatmap.HitObjects.Serialize();
var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize();
Assert.That(newHitObjects, Is.EqualTo(originalHitObjects));
ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
{
// emulate non-legacy control points by cloning the non-legacy portion.
// the assertion is that the encoder can recreate this losslessly from hitobject data.
Assert.IsInstanceOf<LegacyControlPointInfo>(controlPointInfo);
var newControlPoints = new ControlPointInfo();
foreach (var point in controlPointInfo.AllControlPoints)
{
// completely ignore "legacy" types, which have been moved to HitObjects.
// even though these would mostly be ignored by the Add call, they will still be available in groups,
// which isn't what we want to be testing here.
if (point is SampleControlPoint)
continue;
newControlPoints.Add(point.Time, point.DeepClone());
}
return newControlPoints;
}
}
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
@ -132,7 +174,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
private Stream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream();

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
@ -30,7 +31,7 @@ namespace osu.Game.Tests.Editing.Checks
{
check = new CheckMutedObjects();
cpi = new ControlPointInfo();
cpi = new LegacyControlPointInfo();
cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });

View File

@ -4,6 +4,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Tests.NonVisual
{
@ -64,7 +65,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddRedundantSample()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
cpi.Add(0, new SampleControlPoint()); // is *not* redundant, special exception for first sample point
cpi.Add(1000, new SampleControlPoint()); // is redundant
@ -142,7 +143,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestRemoveGroupAlsoRemovedControlPoints()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Editing
beatmap.BeatmapInfo.BeatDivisor = 1;
beatmap.ControlPointInfo = new ControlPointInfo();
beatmap.ControlPointInfo.Clear();
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
beatmap.ControlPointInfo.Add(2000, new TimingControlPoint { BeatLength = 500 });

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using Newtonsoft.Json;
using osu.Game.Graphics;
using osu.Game.Utils;
using osuTK.Graphics;
@ -13,6 +14,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary>
/// The time at which the control point takes effect.
/// </summary>
[JsonIgnore]
public double Time => controlPointGroup?.Time ?? 0;
private ControlPointGroup controlPointGroup;

View File

@ -41,14 +41,6 @@ namespace osu.Game.Beatmaps.ControlPoints
private readonly SortedList<DifficultyControlPoint> difficultyPoints = new SortedList<DifficultyControlPoint>(Comparer<DifficultyControlPoint>.Default);
/// <summary>
/// All sound points.
/// </summary>
[JsonProperty]
public IBindableList<SampleControlPoint> SamplePoints => samplePoints;
private readonly BindableList<SampleControlPoint> samplePoints = new BindableList<SampleControlPoint>();
/// <summary>
/// All effect points.
/// </summary>
@ -69,7 +61,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the difficulty control point at.</param>
/// <returns>The difficulty control point.</returns>
[NotNull]
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
/// <summary>
/// Finds the effect control point that is active at <paramref name="time"/>.
@ -77,15 +69,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the effect control point at.</param>
/// <returns>The effect control point.</returns>
[NotNull]
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
/// <summary>
/// Finds the sound control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the sound control point at.</param>
/// <returns>The sound control point.</returns>
[NotNull]
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
public EffectControlPoint EffectPointAt(double time) => BinarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
/// <summary>
/// Finds the timing control point that is active at <paramref name="time"/>.
@ -93,7 +77,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the timing control point at.</param>
/// <returns>The timing control point.</returns>
[NotNull]
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
public TimingControlPoint TimingPointAt(double time) => BinarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
/// <summary>
/// Finds the maximum BPM represented by any timing control point.
@ -112,12 +96,11 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary>
/// Remove all <see cref="ControlPointGroup"/>s and return to a pristine state.
/// </summary>
public void Clear()
public virtual void Clear()
{
groups.Clear();
timingPoints.Clear();
difficultyPoints.Clear();
samplePoints.Clear();
effectPoints.Clear();
}
@ -129,7 +112,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <returns>Whether the control point was added.</returns>
public bool Add(double time, ControlPoint controlPoint)
{
if (checkAlreadyExisting(time, controlPoint))
if (CheckAlreadyExisting(time, controlPoint))
return false;
GroupAt(time, true).Add(controlPoint);
@ -147,8 +130,8 @@ namespace osu.Game.Beatmaps.ControlPoints
if (addIfNotExisting)
{
newGroup.ItemAdded += groupItemAdded;
newGroup.ItemRemoved += groupItemRemoved;
newGroup.ItemAdded += GroupItemAdded;
newGroup.ItemRemoved += GroupItemRemoved;
groups.Insert(~i, newGroup);
return newGroup;
@ -162,8 +145,8 @@ namespace osu.Game.Beatmaps.ControlPoints
foreach (var item in group.ControlPoints.ToArray())
group.Remove(item);
group.ItemAdded -= groupItemAdded;
group.ItemRemoved -= groupItemRemoved;
group.ItemAdded -= GroupItemAdded;
group.ItemRemoved -= GroupItemRemoved;
groups.Remove(group);
}
@ -228,10 +211,10 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the control point at.</param>
/// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param>
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
private T binarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
protected T BinarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
where T : ControlPoint
{
return binarySearch(list, time) ?? fallback;
return BinarySearch(list, time) ?? fallback;
}
/// <summary>
@ -240,7 +223,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="list">The list to search.</param>
/// <param name="time">The time to find the control point at.</param>
/// <returns>The active control point at <paramref name="time"/>.</returns>
private T binarySearch<T>(IReadOnlyList<T> list, double time)
protected virtual T BinarySearch<T>(IReadOnlyList<T> list, double time)
where T : ControlPoint
{
if (list == null)
@ -280,7 +263,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the timing control point at.</param>
/// <param name="newPoint">A point to be added.</param>
/// <returns>Whether the new point should be added.</returns>
private bool checkAlreadyExisting(double time, ControlPoint newPoint)
protected virtual bool CheckAlreadyExisting(double time, ControlPoint newPoint)
{
ControlPoint existing = null;
@ -288,17 +271,13 @@ namespace osu.Game.Beatmaps.ControlPoints
{
case TimingControlPoint _:
// Timing points are a special case and need to be added regardless of fallback availability.
existing = binarySearch(TimingPoints, time);
existing = BinarySearch(TimingPoints, time);
break;
case EffectControlPoint _:
existing = EffectPointAt(time);
break;
case SampleControlPoint _:
existing = binarySearch(SamplePoints, time);
break;
case DifficultyControlPoint _:
existing = DifficultyPointAt(time);
break;
@ -307,7 +286,7 @@ namespace osu.Game.Beatmaps.ControlPoints
return newPoint?.IsRedundant(existing) == true;
}
private void groupItemAdded(ControlPoint controlPoint)
protected virtual void GroupItemAdded(ControlPoint controlPoint)
{
switch (controlPoint)
{
@ -319,17 +298,13 @@ namespace osu.Game.Beatmaps.ControlPoints
effectPoints.Add(typed);
break;
case SampleControlPoint typed:
samplePoints.Add(typed);
break;
case DifficultyControlPoint typed:
difficultyPoints.Add(typed);
break;
}
}
private void groupItemRemoved(ControlPoint controlPoint)
protected virtual void GroupItemRemoved(ControlPoint controlPoint)
{
switch (controlPoint)
{
@ -341,10 +316,6 @@ namespace osu.Game.Beatmaps.ControlPoints
effectPoints.Remove(typed);
break;
case SampleControlPoint typed:
samplePoints.Remove(typed);
break;
case DifficultyControlPoint typed:
difficultyPoints.Remove(typed);
break;
@ -353,7 +324,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public ControlPointInfo DeepClone()
{
var controlPointInfo = new ControlPointInfo();
var controlPointInfo = (ControlPointInfo)Activator.CreateInstance(GetType());
foreach (var point in AllControlPoints)
controlPointInfo.Add(point.Time, point.DeepClone());

View File

@ -44,6 +44,13 @@ namespace osu.Game.Beatmaps.Formats
offset = FormatVersion < 5 ? 24 : 0;
}
protected override Beatmap CreateTemplateObject()
{
var templateBeatmap = base.CreateTemplateObject();
templateBeatmap.ControlPointInfo = new LegacyControlPointInfo();
return templateBeatmap;
}
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatmap)
{
this.beatmap = beatmap;
@ -430,8 +437,13 @@ namespace osu.Game.Beatmaps.Formats
parser ??= new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
var obj = parser.Parse(line);
if (obj != null)
{
obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
beatmap.HitObjects.Add(obj);
}
}
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);

View File

@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}"));
writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}"));
writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}"));
writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank(beatmap.ControlPointInfo.SamplePointAt(double.MinValue).SampleBank)}"));
writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank((beatmap.HitObjects.FirstOrDefault()?.SampleControlPoint ?? SampleControlPoint.DEFAULT).SampleBank)}"));
writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}"));
writer.WriteLine(FormattableString.Invariant($"Mode: {beatmap.BeatmapInfo.RulesetID}"));
writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}"));
@ -172,6 +172,30 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[TimingPoints]");
if (!(beatmap.ControlPointInfo is LegacyControlPointInfo))
{
var legacyControlPoints = new LegacyControlPointInfo();
foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
legacyControlPoints.Add(point.Time, point.DeepClone());
beatmap.ControlPointInfo = legacyControlPoints;
SampleControlPoint lastRelevantSamplePoint = null;
// iterate over hitobjects and pull out all required sample changes
foreach (var h in beatmap.HitObjects)
{
var hSamplePoint = h.SampleControlPoint;
if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint))
{
legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint);
lastRelevantSamplePoint = hSamplePoint;
}
}
}
foreach (var group in beatmap.ControlPointInfo.Groups)
{
var groupTimingPoint = group.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
@ -181,19 +205,19 @@ namespace osu.Game.Beatmaps.Formats
{
writer.Write(FormattableString.Invariant($"{groupTimingPoint.Time},"));
writer.Write(FormattableString.Invariant($"{groupTimingPoint.BeatLength},"));
outputControlPointEffectsAt(groupTimingPoint.Time, true);
outputControlPointAt(groupTimingPoint.Time, true);
}
// Output any remaining effects as secondary non-timing control point.
var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time);
writer.Write(FormattableString.Invariant($"{group.Time},"));
writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},"));
outputControlPointEffectsAt(group.Time, false);
outputControlPointAt(group.Time, false);
}
void outputControlPointEffectsAt(double time, bool isTimingPoint)
void outputControlPointAt(double time, bool isTimingPoint)
{
var samplePoint = beatmap.ControlPointInfo.SamplePointAt(time);
var samplePoint = ((LegacyControlPointInfo)beatmap.ControlPointInfo).SamplePointAt(time);
var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time);
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)

View File

@ -0,0 +1,62 @@
// 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 JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Beatmaps.Legacy
{
public class LegacyControlPointInfo : ControlPointInfo
{
/// <summary>
/// All sound points.
/// </summary>
[JsonProperty]
public IBindableList<SampleControlPoint> SamplePoints => samplePoints;
private readonly BindableList<SampleControlPoint> samplePoints = new BindableList<SampleControlPoint>();
/// <summary>
/// Finds the sound control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the sound control point at.</param>
/// <returns>The sound control point.</returns>
[NotNull]
public SampleControlPoint SamplePointAt(double time) => BinarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
public override void Clear()
{
base.Clear();
samplePoints.Clear();
}
protected override bool CheckAlreadyExisting(double time, ControlPoint newPoint)
{
if (newPoint is SampleControlPoint)
{
var existing = BinarySearch(SamplePoints, time);
return newPoint.IsRedundant(existing);
}
return base.CheckAlreadyExisting(time, newPoint);
}
protected override void GroupItemAdded(ControlPoint controlPoint)
{
if (controlPoint is SampleControlPoint typed)
samplePoints.Add(typed);
base.GroupItemAdded(controlPoint);
}
protected override void GroupItemRemoved(ControlPoint controlPoint)
{
if (controlPoint is SampleControlPoint typed)
samplePoints.Remove(typed);
base.GroupItemRemoved(controlPoint);
}
}
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@ -65,7 +66,6 @@ namespace osu.Game.Rulesets.Objects
}
}
[JsonIgnore]
public SampleControlPoint SampleControlPoint;
/// <summary>
@ -106,8 +106,15 @@ namespace osu.Game.Rulesets.Objects
{
ApplyDefaultsToSelf(controlPointInfo, difficulty);
// This is done here since ApplyDefaultsToSelf may be used to determine the end time
SampleControlPoint = controlPointInfo.SamplePointAt(this.GetEndTime() + control_point_leniency);
if (controlPointInfo is LegacyControlPointInfo legacyInfo)
{
// This is done here since ApplyDefaultsToSelf may be used to determine the end time
SampleControlPoint = legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency);
}
else
{
SampleControlPoint ??= SampleControlPoint.DEFAULT;
}
nestedHitObjects.Clear();

View File

@ -42,15 +42,6 @@ namespace osu.Game.Screens.Edit.Timing
}
}
protected override SampleControlPoint CreatePoint()
{
var reference = Beatmap.ControlPointInfo.SamplePointAt(SelectedGroup.Value.Time);
return new SampleControlPoint
{
SampleBank = reference.SampleBank,
SampleVolume = reference.SampleVolume,
};
}
protected override SampleControlPoint CreatePoint() => new SampleControlPoint(); // TODO: remove
}
}