1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-11 03:32:55 +08:00

Merge pull request #23308 from OliBomby/sample-control-points

Remove SampleControlPoint and DifficultyControlPoint from HitObject
This commit is contained in:
Dean Herbert 2023-05-03 14:21:51 +09:00 committed by GitHub
commit 6b017ac05f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 457 additions and 417 deletions

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1])); AddAssert("end time is correct", () => Precision.AlmostEquals(lastObject.EndTime, times[1]));
AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0])); AddAssert("start position is correct", () => Precision.AlmostEquals(lastObject.OriginalX, positions[0]));
AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1])); AddAssert("end position is correct", () => Precision.AlmostEquals(lastObject.EndX, positions[1]));
AddAssert("default slider velocity", () => lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); AddAssert("default slider velocity", () => lastObject.SliderVelocityBindable.IsDefault);
} }
[Test] [Test]
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
addPlacementSteps(times, positions); addPlacementSteps(times, positions);
addPathCheckStep(times, positions); addPathCheckStep(times, positions);
AddAssert("slider velocity changed", () => !lastObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); AddAssert("slider velocity changed", () => !lastObject.SliderVelocityBindable.IsDefault);
} }
[Test] [Test]

View File

@ -108,11 +108,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
double[] times = { 100, 300 }; double[] times = { 100, 300 };
float[] positions = { 200, 300 }; float[] positions = { 200, 300 };
addBlueprintStep(times, positions); addBlueprintStep(times, positions);
AddAssert("default slider velocity", () => hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); AddAssert("default slider velocity", () => hitObject.SliderVelocityBindable.IsDefault);
addDragStartStep(times[1], positions[1]); addDragStartStep(times[1], positions[1]);
AddMouseMoveStep(times[1], 400); AddMouseMoveStep(times[1], 400);
AddAssert("slider velocity changed", () => !hitObject.DifficultyControlPoint.SliderVelocityBindable.IsDefault); AddAssert("slider velocity changed", () => !hitObject.SliderVelocityBindable.IsDefault);
} }
[Test] [Test]

View File

@ -26,6 +26,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
var xPositionData = obj as IHasXPosition; var xPositionData = obj as IHasXPosition;
var yPositionData = obj as IHasYPosition; var yPositionData = obj as IHasYPosition;
var comboData = obj as IHasCombo; var comboData = obj as IHasCombo;
var sliderVelocityData = obj as IHasSliderVelocity;
switch (obj) switch (obj)
{ {
@ -41,7 +42,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
NewCombo = comboData?.NewCombo ?? false, NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0, ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0, LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1
}.Yield(); }.Yield();
case IHasDuration endTime: case IHasDuration endTime:

View File

@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
public void UpdateHitObjectFromPath(JuiceStream hitObject) public void UpdateHitObjectFromPath(JuiceStream hitObject)
{ {
// The SV setting may need to be changed for the current path. // The SV setting may need to be changed for the current path.
var svBindable = hitObject.DifficultyControlPoint.SliderVelocityBindable; var svBindable = hitObject.SliderVelocityBindable;
double svToVelocityFactor = hitObject.Velocity / svBindable.Value; double svToVelocityFactor = hitObject.Velocity / svBindable.Value;
double requiredVelocity = path.ComputeRequiredVelocity(); double requiredVelocity = path.ComputeRequiredVelocity();

View File

@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Catch.Objects
public override Judgement CreateJudgement() => new CatchBananaJudgement(); public override Judgement CreateJudgement() => new CatchBananaJudgement();
private static readonly List<HitSampleInfo> samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }; private static readonly IList<HitSampleInfo> default_banana_samples = new List<HitSampleInfo> { new BananaHitSampleInfo() }.AsReadOnly();
public Banana() public Banana()
{ {
Samples = samples; Samples = default_banana_samples;
} }
// override any external colour changes with banananana // override any external colour changes with banananana
@ -47,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
} }
private class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo> public class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo>
{ {
private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" }; private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" };
public override IEnumerable<string> LookupNames => lookup_names; public override IEnumerable<string> LookupNames => lookup_names;
public BananaHitSampleInfo(int volume = 0) public BananaHitSampleInfo(int volume = 100)
: base(string.Empty, volume: volume) : base(string.Empty, volume: volume)
{ {
} }

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Threading; using System.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
StartTime = time, StartTime = time,
BananaIndex = i, BananaIndex = i,
Samples = new List<HitSampleInfo> { new Banana.BananaHitSampleInfo(GetSampleInfo().Volume) }
}); });
time += spacing; time += spacing;

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -16,7 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects namespace osu.Game.Rulesets.Catch.Objects
{ {
public class JuiceStream : CatchHitObject, IHasPathWithRepeats public class JuiceStream : CatchHitObject, IHasPathWithRepeats, IHasSliderVelocity
{ {
/// <summary> /// <summary>
/// Positional distance that results in a duration of one second, before any speed adjustments. /// Positional distance that results in a duration of one second, before any speed adjustments.
@ -27,6 +28,19 @@ namespace osu.Game.Rulesets.Catch.Objects
public int RepeatCount { get; set; } public int RepeatCount { get; set; }
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
[JsonIgnore] [JsonIgnore]
private double velocityFactor; private double velocityFactor;
@ -34,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Objects
private double tickDistanceFactor; private double tickDistanceFactor;
[JsonIgnore] [JsonIgnore]
public double Velocity => velocityFactor * DifficultyControlPoint.SliderVelocity; public double Velocity => velocityFactor * SliderVelocity;
[JsonIgnore] [JsonIgnore]
public double TickDistance => tickDistanceFactor * DifficultyControlPoint.SliderVelocity; public double TickDistance => tickDistanceFactor * SliderVelocity;
/// <summary> /// <summary>
/// The length of one span of this <see cref="JuiceStream"/>. /// The length of one span of this <see cref="JuiceStream"/>.

View File

@ -14,7 +14,6 @@ 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;
using osu.Game.Utils; using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
@ -49,15 +48,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Debug.Assert(distanceData != null); Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
double beatLength; double beatLength;
#pragma warning disable 618 if (hitObject.LegacyBpmMultiplier.HasValue)
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint) beatLength = timingPoint.BeatLength * hitObject.LegacyBpmMultiplier.Value;
#pragma warning restore 618 else if (hitObject is IHasSliderVelocity hasSliderVelocity)
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
else else
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; beatLength = timingPoint.BeatLength;
SpanCount = repeatsData?.SpanCount() ?? 1; SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime); StartTime = (int)Math.Round(hitObject.StartTime);

View File

@ -350,13 +350,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
if (HitObject.SampleControlPoint == null) slidingSample.Samples = HitObject.CreateSlidingSamples().Cast<ISampleInfo>().ToArray();
{
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
} }
public override void StopAllSamples() public override void StopAllSamples()

View File

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

View File

@ -138,8 +138,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First(); var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples) return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples) && mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
&& mergedSlider.Samples.SequenceEqual(slider1.Samples) && mergedSlider.Samples.SequenceEqual(slider1.Samples);
&& mergedSlider.SampleControlPoint.IsRedundant(slider1.SampleControlPoint);
}); });
AddAssert("slider end is at same completion for last slider", () => AddAssert("slider end is at same completion for last slider", () =>

View File

@ -181,10 +181,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
if (slider is null) return; if (slider is null) return;
slider.SampleControlPoint.SampleBank = "soft"; sample = new HitSampleInfo("hitwhistle", "soft", volume: 70);
slider.SampleControlPoint.SampleVolume = 70; slider.Samples.Add(sample.With());
sample = new HitSampleInfo("hitwhistle");
slider.Samples.Add(sample);
}); });
AddStep("select added slider", () => AddStep("select added slider", () =>
@ -207,9 +205,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("sliders have hitsounds", hasHitsounds); AddAssert("sliders have hitsounds", hasHitsounds);
bool hasHitsounds() => sample is not null && bool hasHitsounds() => sample is not null &&
EditorBeatmap.HitObjects.All(o => o.SampleControlPoint.SampleBank == "soft" && EditorBeatmap.HitObjects.All(o => o.Samples.Contains(sample));
o.SampleControlPoint.SampleVolume == 70 &&
o.Samples.Contains(sample));
} }
private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints) private bool sliderCreatedFor(Slider s, double startTime, double endTime, params (Vector2 pos, PathType? pathType)[] expectedControlPoints)

View File

@ -199,8 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
Precision.AlmostEquals(circle.StartTime, time, 1) Precision.AlmostEquals(circle.StartTime, time, 1)
&& Precision.AlmostEquals(circle.Position, position, 0.01f) && Precision.AlmostEquals(circle.Position, position, 0.01f)
&& circle.NewCombo == startsNewCombo && circle.NewCombo == startsNewCombo
&& circle.Samples.SequenceEqual(slider.HeadCircle.Samples) && circle.Samples.SequenceEqual(slider.HeadCircle.Samples);
&& circle.SampleControlPoint.IsRedundant(slider.SampleControlPoint);
} }
private bool sliderRestored(Slider slider) private bool sliderRestored(Slider slider)

View File

@ -439,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public TestSlider() public TestSlider()
{ {
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; SliderVelocity = 0.1f;
DefaultsApplied += _ => DefaultsApplied += _ =>
{ {

View File

@ -7,7 +7,6 @@ using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
StartTime = time_slider_start, StartTime = time_slider_start,
Position = new Vector2(0, 0), Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = velocity }, SliderVelocity = velocity,
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,

View File

@ -8,7 +8,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -350,7 +349,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
StartTime = time_slider_start, StartTime = time_slider_start,
Position = new Vector2(0, 0), Position = new Vector2(0, 0),
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }, SliderVelocity = 0.1f,
Path = new SliderPath(PathType.PerfectCurve, new[] Path = new SliderPath(PathType.PerfectCurve, new[]
{ {
Vector2.Zero, Vector2.Zero,

View File

@ -399,7 +399,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public TestSlider() public TestSlider()
{ {
DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; SliderVelocity = 0.1f;
DefaultsApplied += _ => DefaultsApplied += _ =>
{ {

View File

@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
var positionData = original as IHasPosition; var positionData = original as IHasPosition;
var comboData = original as IHasCombo; var comboData = original as IHasCombo;
var sliderVelocityData = original as IHasSliderVelocity;
var generateTicksData = original as IHasGenerateTicks;
switch (original) switch (original)
{ {
@ -47,7 +49,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset, LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration. // this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1 TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
SliderVelocity = sliderVelocityData?.SliderVelocity ?? 1,
}.Yield(); }.Yield();
case IHasDuration endTimeData: case IHasDuration endTimeData:

View File

@ -10,7 +10,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -83,11 +82,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
case SliderPlacementState.Initial: case SliderPlacementState.Initial:
BeginPlacement(); BeginPlacement();
var nearestDifficultyPoint = editorBeatmap.HitObjects double? nearestSliderVelocity = (editorBeatmap.HitObjects
.LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime)? .LastOrDefault(h => h is Slider && h.GetEndTime() < HitObject.StartTime) as Slider)?.SliderVelocity;
.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint(); HitObject.SliderVelocity = nearestSliderVelocity ?? 1;
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
// Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation. // Replacing the DifficultyControlPoint above doesn't trigger any kind of invalidation.

View File

@ -14,7 +14,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -311,17 +310,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var splitControlPoints = controlPoints.Take(index + 1).ToList(); var splitControlPoints = controlPoints.Take(index + 1).ToList();
controlPoints.RemoveRange(0, index); controlPoints.RemoveRange(0, index);
// Turn the control points which were split off into a new slider.
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
var difficultyPoint = (DifficultyControlPoint)HitObject.DifficultyControlPoint.DeepClone();
var newSlider = new Slider var newSlider = new Slider
{ {
StartTime = HitObject.StartTime, StartTime = HitObject.StartTime,
Position = HitObject.Position + splitControlPoints[0].Position, Position = HitObject.Position + splitControlPoints[0].Position,
NewCombo = HitObject.NewCombo, NewCombo = HitObject.NewCombo,
SampleControlPoint = samplePoint,
DifficultyControlPoint = difficultyPoint,
LegacyLastTickOffset = HitObject.LegacyLastTickOffset, LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
Samples = HitObject.Samples.Select(s => s.With()).ToList(), Samples = HitObject.Samples.Select(s => s.With()).ToList(),
RepeatCount = HitObject.RepeatCount, RepeatCount = HitObject.RepeatCount,
@ -378,15 +371,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition); Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
samplePoint.Time = time;
editorBeatmap.Add(new HitCircle editorBeatmap.Add(new HitCircle
{ {
StartTime = time, StartTime = time,
Position = position, Position = position,
NewCombo = i == 0 && HitObject.NewCombo, NewCombo = i == 0 && HitObject.NewCombo,
SampleControlPoint = samplePoint,
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList() Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
}); });

View File

@ -362,7 +362,6 @@ namespace osu.Game.Rulesets.Osu.Edit
StartTime = firstHitObject.StartTime, StartTime = firstHitObject.StartTime,
Position = firstHitObject.Position, Position = firstHitObject.Position,
NewCombo = firstHitObject.NewCombo, NewCombo = firstHitObject.NewCombo,
SampleControlPoint = firstHitObject.SampleControlPoint,
Samples = firstHitObject.Samples, Samples = firstHitObject.Samples,
}; };

View File

@ -133,14 +133,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being. // Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
if (HitObject.SampleControlPoint == null) Samples.Samples = HitObject.TailSamples.Cast<ISampleInfo>().ToArray();
{ slidingSample.Samples = HitObject.CreateSlidingSamples().Cast<ISampleInfo>().ToArray();
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
} }
public override void StopAllSamples() public override void StopAllSamples()

View File

@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.LoadSamples(); base.LoadSamples();
spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray(); spinningSample.Samples = HitObject.CreateSpinningSamples().Cast<ISampleInfo>().ToArray();
spinningSample.Frequency.Value = spinning_sample_initial_frequency; spinningSample.Frequency.Value = spinning_sample_initial_frequency;
} }

View File

@ -10,18 +10,18 @@ using osu.Game.Rulesets.Objects;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
public class Slider : OsuHitObject, IHasPathWithRepeats public class Slider : OsuHitObject, IHasPathWithRepeats, IHasSliderVelocity, IHasGenerateTicks
{ {
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
@ -134,6 +134,21 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public bool OnlyJudgeNestedObjects = true; public bool OnlyJudgeNestedObjects = true;
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
public bool GenerateTicks { get; set; } = true;
[JsonIgnore] [JsonIgnore]
public SliderHeadCircle HeadCircle { get; protected set; } public SliderHeadCircle HeadCircle { get; protected set; }
@ -151,15 +166,11 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
#pragma warning disable 618
var legacyDifficultyPoint = DifficultyControlPoint as LegacyBeatmapDecoder.LegacyDifficultyControlPoint;
#pragma warning restore 618
double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * SliderVelocity;
bool generateTicks = legacyDifficultyPoint?.GenerateTicks ?? true;
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = generateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity; TickDistance = GenerateTicks ? (scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier) : double.PositiveInfinity;
} }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Objects
AddNested(i < SpinsRequired AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration } ? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration }); : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { GetSampleInfo("spinnerbonus") } });
} }
} }
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects
return new[] return new[]
{ {
SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin") referenceSample.With("spinnerspin")
}; };
} }
} }

View File

@ -3,7 +3,6 @@
#nullable disable #nullable disable
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -11,11 +10,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
public class SpinnerBonusTick : SpinnerTick public class SpinnerBonusTick : SpinnerTick
{ {
public SpinnerBonusTick()
{
Samples.Add(new HitSampleInfo("spinnerbonus"));
}
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement(); public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement

View File

@ -64,7 +64,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
foreach (HitObject hitObject in original.HitObjects) foreach (HitObject hitObject in original.HitObjects)
{ {
double nextScrollSpeed = hitObject.DifficultyControlPoint.SliderVelocity; if (hitObject is not IHasSliderVelocity hasSliderVelocity) continue;
double nextScrollSpeed = hasSliderVelocity.SliderVelocity;
EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime); EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision)) if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
@ -131,7 +133,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
Duration = taikoDuration, Duration = taikoDuration,
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4 TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
}; };
} }
@ -177,15 +180,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint;
double beatLength; double beatLength;
#pragma warning disable 618 if (obj.LegacyBpmMultiplier.HasValue)
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint) beatLength = timingPoint.BeatLength * obj.LegacyBpmMultiplier.Value;
#pragma warning restore 618 else if (obj is IHasSliderVelocity hasSliderVelocity)
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; beatLength = timingPoint.BeatLength / hasSliderVelocity.SliderVelocity;
else else
beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; beatLength = timingPoint.BeatLength;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate; double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;

View File

@ -3,8 +3,11 @@
#nullable disable #nullable disable
using System.Linq;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Threading; using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
@ -15,7 +18,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
{ {
public class DrumRoll : TaikoStrongableHitObject, IHasPath public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity
{ {
/// <summary> /// <summary>
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length. /// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@ -35,6 +38,19 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary> /// </summary>
public double Velocity { get; private set; } public double Velocity { get; private set; }
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
/// <summary> /// <summary>
/// Numer of ticks per beat length. /// Numer of ticks per beat length.
/// </summary> /// </summary>
@ -52,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate; tickSpacing = timingPoint.BeatLength / TickRate;
@ -81,7 +97,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
FirstTick = first, FirstTick = first,
TickSpacing = tickSpacing, TickSpacing = tickSpacing,
StartTime = t, StartTime = t,
IsStrong = IsStrong IsStrong = IsStrong,
Samples = Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToList()
}); });
first = false; first = false;

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
if (IsStrongBindable.Value != strongSamples.Any()) if (IsStrongBindable.Value != strongSamples.Any())
{ {
if (IsStrongBindable.Value) if (IsStrongBindable.Value)
Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); Samples.Add(GetSampleInfo(HitSampleInfo.HIT_FINISH));
else else
{ {
foreach (var sample in strongSamples) foreach (var sample in strongSamples)

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -17,12 +18,12 @@ namespace osu.Game.Rulesets.Taiko.UI
public void Play(HitType hitType) public void Play(HitType hitType)
{ {
var hitObject = GetMostValidObject(); var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
if (hitObject == null) if (hitSample == null)
return; return;
PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) }); PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) });
} }
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead"); public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");

View File

@ -510,7 +510,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First());
} }
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
} }
[Test] [Test]
@ -528,7 +528,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Gameplay/normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); Assert.AreEqual("Gameplay/normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
} }
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
} }
[Test] [Test]
@ -548,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume); Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume);
} }
static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.Samples[0];
} }
[Test] [Test]

View File

@ -37,45 +37,6 @@ namespace osu.Game.Tests.Editing.Checks
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted }); cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
} }
[Test]
public void TestNormalControlPointVolume()
{
var hitCircle = new HitCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { hitCircle });
}
[Test]
public void TestLowControlPointVolume()
{
var hitCircle = new HitCircle
{
StartTime = 1000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertLowVolume(new List<HitObject> { hitCircle });
}
[Test]
public void TestMutedControlPointVolume()
{
var hitCircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitCircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { hitCircle });
}
[Test] [Test]
public void TestNormalSampleVolume() public void TestNormalSampleVolume()
{ {
@ -122,7 +83,7 @@ namespace osu.Game.Tests.Editing.Checks
var sliderHead = new SliderHeadCircle var sliderHead = new SliderHeadCircle
{ {
StartTime = 0, StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
}; };
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
@ -135,7 +96,7 @@ namespace osu.Game.Tests.Editing.Checks
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500) var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{ {
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
}; };
slider.ApplyDefaults(cpi, new BeatmapDifficulty()); slider.ApplyDefaults(cpi, new BeatmapDifficulty());
@ -155,13 +116,13 @@ namespace osu.Game.Tests.Editing.Checks
var sliderTick = new SliderTick var sliderTick = new SliderTick
{ {
StartTime = 250, StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") } Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_regular) }
}; };
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500) var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{ {
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail. Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) } // Applies to the tail.
}; };
slider.ApplyDefaults(cpi, new BeatmapDifficulty()); slider.ApplyDefaults(cpi, new BeatmapDifficulty());
@ -174,14 +135,14 @@ namespace osu.Game.Tests.Editing.Checks
var sliderHead = new SliderHeadCircle var sliderHead = new SliderHeadCircle
{ {
StartTime = 0, StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
}; };
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty()); sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick var sliderTick = new SliderTick
{ {
StartTime = 250, StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") } Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_regular) }
}; };
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty()); sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
@ -194,59 +155,6 @@ namespace osu.Game.Tests.Editing.Checks
assertMutedPassive(new List<HitObject> { slider }); assertMutedPassive(new List<HitObject> { slider });
} }
[Test]
public void TestMutedControlPointVolumeSliderHead()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 2250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { slider });
}
[Test]
public void TestMutedControlPointVolumeSliderTail()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
// Ends after the 5% control point.
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMutedPassive(new List<HitObject> { slider });
}
private void assertOk(List<HitObject> hitObjects) private void assertOk(List<HitObject> hitObjects)
{ {
Assert.That(check.Run(getContext(hitObjects)), Is.Empty); Assert.That(check.Run(getContext(hitObjects)), Is.Empty);

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -74,12 +75,9 @@ namespace osu.Game.Tests.Editing
[TestCase(2)] [TestCase(2)]
public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier) public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{ {
assertSnapDistance(100, new HitObject assertSnapDistance(100, new Slider
{
DifficultyControlPoint = new DifficultyControlPoint
{ {
SliderVelocity = multiplier SliderVelocity = multiplier
}
}, false); }, false);
} }
@ -87,12 +85,9 @@ namespace osu.Game.Tests.Editing
[TestCase(2)] [TestCase(2)]
public void TestSpeedMultiplierDoesChangeDistanceSnap(float multiplier) public void TestSpeedMultiplierDoesChangeDistanceSnap(float multiplier)
{ {
assertSnapDistance(100 * multiplier, new HitObject assertSnapDistance(100 * multiplier, new Slider
{
DifficultyControlPoint = new DifficultyControlPoint
{ {
SliderVelocity = multiplier SliderVelocity = multiplier
}
}, true); }, true);
} }
@ -114,12 +109,9 @@ namespace osu.Game.Tests.Editing
const float base_distance = 100; const float base_distance = 100;
const float slider_velocity = 1.2f; const float slider_velocity = 1.2f;
var referenceObject = new HitObject var referenceObject = new Slider
{
DifficultyControlPoint = new DifficultyControlPoint
{ {
SliderVelocity = slider_velocity SliderVelocity = slider_velocity
}
}; };
assertSnapDistance(base_distance * slider_velocity, referenceObject, true); assertSnapDistance(base_distance * slider_velocity, referenceObject, true);

View File

@ -6,7 +6,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -95,10 +94,6 @@ namespace osu.Game.Tests.Visual.Editing
var path = slider.Path; var path = slider.Path;
return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints); return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints);
}); });
// see `HitObject.control_point_leniency`.
AddAssert("sample control point has correct time", () => Precision.AlmostEquals(slider.SampleControlPoint.Time, slider.GetEndTime(), 1));
AddAssert("difficulty control point has correct time", () => slider.DifficultyControlPoint.Time == slider.StartTime);
} }
[Test] [Test]

View File

@ -122,19 +122,9 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500); AddAssert("Beatmap has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
// After placement these must be non-default as defaults are read-only.
AddAssert("Placed object has non-default control points", () =>
!ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
ReloadEditorToSameBeatmap(); ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500); AddAssert("Beatmap still has correct timing point", () => EditorBeatmap.ControlPointInfo.TimingPoints.Single().Time == 500);
// After placement these must be non-default as defaults are read-only.
AddAssert("Placed object still has non-default control points", () =>
!ReferenceEquals(EditorBeatmap.HitObjects[0].SampleControlPoint, SampleControlPoint.DEFAULT) &&
!ReferenceEquals(EditorBeatmap.HitObjects[0].DifficultyControlPoint, DifficultyControlPoint.DEFAULT));
} }
[Test] [Test]

View File

@ -8,10 +8,10 @@ using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
@ -61,10 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
new PathControlPoint(new Vector2(100, 0)) new PathControlPoint(new Vector2(100, 0))
} }
}, },
DifficultyControlPoint = new DifficultyControlPoint
{
SliderVelocity = 2 SliderVelocity = 2
}
}); });
}); });
} }
@ -100,8 +97,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("unify slider velocity", () => AddStep("unify slider velocity", () =>
{ {
foreach (var h in EditorBeatmap.HitObjects) foreach (var h in EditorBeatmap.HitObjects.OfType<IHasSliderVelocity>())
h.DifficultyControlPoint.SliderVelocity = 1.5; h.SliderVelocity = 1.5;
}); });
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@ -185,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () => private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
{ {
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.DifficultyControlPoint.SliderVelocity == velocity; return h is IHasSliderVelocity hasSliderVelocity && hasSliderVelocity.SliderVelocity == velocity;
}); });
} }
} }

View File

@ -4,11 +4,12 @@
#nullable disable #nullable disable
using System.Linq; using System.Linq;
using System.Collections.Generic;
using Humanizer; using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -39,10 +40,9 @@ namespace osu.Game.Tests.Visual.Editing
{ {
StartTime = 0, StartTime = 0,
Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2, Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
SampleControlPoint = new SampleControlPoint Samples = new List<HitSampleInfo>
{ {
SampleBank = "normal", new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 80)
SampleVolume = 80
} }
}); });
@ -50,10 +50,9 @@ namespace osu.Game.Tests.Visual.Editing
{ {
StartTime = 500, StartTime = 500,
Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2, Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
SampleControlPoint = new SampleControlPoint Samples = new List<HitSampleInfo>
{ {
SampleBank = "soft", new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft", volume: 60)
SampleVolume = 60
} }
}); });
}); });
@ -96,7 +95,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("unify sample volume", () => AddStep("unify sample volume", () =>
{ {
foreach (var h in EditorBeatmap.HitObjects) foreach (var h in EditorBeatmap.HitObjects)
h.SampleControlPoint.SampleVolume = 50; {
for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newVolume: 50);
}
}
}); });
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@ -136,7 +140,12 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("unify sample bank", () => AddStep("unify sample bank", () =>
{ {
foreach (var h in EditorBeatmap.HitObjects) foreach (var h in EditorBeatmap.HitObjects)
h.SampleControlPoint.SampleBank = "soft"; {
for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newBank: "soft");
}
}
}); });
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
@ -248,7 +257,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () => private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () =>
{ {
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.SampleControlPoint.SampleVolume == volume; return h.Samples.All(o => o.Volume == volume);
}); });
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () => private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
@ -265,7 +274,7 @@ namespace osu.Game.Tests.Visual.Editing
private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () => private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () =>
{ {
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.SampleControlPoint.SampleBank == bank; return h.Samples.All(o => o.Bank == bank);
}); });
} }
} }

View File

@ -73,8 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay
new HitCircle new HitCircle
{ {
StartTime = t += spacing, StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "soft") },
SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
}, },
new HitCircle new HitCircle
{ {
@ -84,8 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
StartTime = t += spacing, StartTime = t += spacing,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }), Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, "soft") },
SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
}, },
}); });

View File

@ -30,7 +30,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public readonly Bindable<string> SampleBankBindable = new Bindable<string>(DEFAULT_BANK) { Default = DEFAULT_BANK }; public readonly Bindable<string> SampleBankBindable = new Bindable<string>(DEFAULT_BANK) { Default = DEFAULT_BANK };
/// <summary> /// <summary>
/// The speed multiplier at this control point. /// The default sample bank at this control point.
/// </summary> /// </summary>
public string SampleBank public string SampleBank
{ {
@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps.ControlPoints
} }
/// <summary> /// <summary>
/// The default sample bank at this control point. /// The default sample volume at this control point.
/// </summary> /// </summary>
public readonly BindableInt SampleVolumeBindable = new BindableInt(100) public readonly BindableInt SampleVolumeBindable = new BindableInt(100)
{ {

View File

@ -3,6 +3,8 @@
#nullable disable #nullable disable
#pragma warning disable 618
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -15,7 +17,9 @@ using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Beatmaps.Formats namespace osu.Game.Beatmaps.Formats
{ {
@ -26,6 +30,11 @@ namespace osu.Game.Beatmaps.Formats
/// </summary> /// </summary>
public const int EARLY_VERSION_TIMING_OFFSET = 24; public const int EARLY_VERSION_TIMING_OFFSET = 24;
/// <summary>
/// A small adjustment to the start time of control points to account for rounding/precision errors.
/// </summary>
private const double control_point_leniency = 1;
internal static RulesetStore RulesetStore; internal static RulesetStore RulesetStore;
private Beatmap beatmap; private Beatmap beatmap;
@ -85,7 +94,45 @@ namespace osu.Game.Beatmaps.Formats
this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList(); this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList();
foreach (var hitObject in this.beatmap.HitObjects) foreach (var hitObject in this.beatmap.HitObjects)
hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.Difficulty); {
applyDefaults(hitObject);
applySamples(hitObject);
}
}
private void applyDefaults(HitObject hitObject)
{
DifficultyControlPoint difficultyControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.DifficultyPointAt(hitObject.StartTime) ?? DifficultyControlPoint.DEFAULT;
if (difficultyControlPoint is LegacyDifficultyControlPoint legacyDifficultyControlPoint)
{
hitObject.LegacyBpmMultiplier = legacyDifficultyControlPoint.BpmMultiplier;
if (hitObject is IHasGenerateTicks hasGenerateTicks)
hasGenerateTicks.GenerateTicks = legacyDifficultyControlPoint.GenerateTicks;
}
if (hitObject is IHasSliderVelocity hasSliderVelocity)
hasSliderVelocity.SliderVelocity = difficultyControlPoint.SliderVelocity;
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
}
private void applySamples(HitObject hitObject)
{
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + control_point_leniency) ?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
if (hitObject is IHasRepeats hasRepeats)
{
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
{
double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + control_point_leniency;
var nodeSamplePoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(time) ?? SampleControlPoint.DEFAULT;
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(o => nodeSamplePoint.ApplyTo(o)).ToList();
}
}
} }
/// <summary> /// <summary>
@ -451,9 +498,7 @@ namespace osu.Game.Beatmaps.Formats
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID; int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
#pragma warning disable 618
addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength) addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength)
#pragma warning restore 618
{ {
SliderVelocity = speedMultiplier, SliderVelocity = speedMultiplier,
}, timingChange); }, timingChange);

View File

@ -92,7 +92,8 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}")); writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}"));
writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}"));
writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}"));
writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank((beatmap.HitObjects.FirstOrDefault()?.SampleControlPoint ?? SampleControlPoint.DEFAULT).SampleBank)}")); writer.WriteLine(FormattableString.Invariant(
$"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints?.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}"));
writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}"));
writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}")); writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}"));
writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}"));
@ -173,9 +174,6 @@ namespace osu.Game.Beatmaps.Formats
private void handleControlPoints(TextWriter writer) private void handleControlPoints(TextWriter writer)
{ {
if (beatmap.ControlPointInfo.Groups.Count == 0)
return;
var legacyControlPoints = new LegacyControlPointInfo(); var legacyControlPoints = new LegacyControlPointInfo();
foreach (var point in beatmap.ControlPointInfo.AllControlPoints) foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
legacyControlPoints.Add(point.Time, point.DeepClone()); legacyControlPoints.Add(point.Time, point.DeepClone());
@ -199,33 +197,43 @@ namespace osu.Game.Beatmaps.Formats
legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed }); legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed });
} }
LegacyControlPointProperties lastControlPointProperties = new LegacyControlPointProperties();
foreach (var group in legacyControlPoints.Groups) foreach (var group in legacyControlPoints.Groups)
{ {
var groupTimingPoint = group.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault(); var groupTimingPoint = group.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
var controlPointProperties = getLegacyControlPointProperties(group, groupTimingPoint != null);
// If the group contains a timing control point, it needs to be output separately. // If the group contains a timing control point, it needs to be output separately.
if (groupTimingPoint != null) if (groupTimingPoint != null)
{ {
writer.Write(FormattableString.Invariant($"{groupTimingPoint.Time},")); writer.Write(FormattableString.Invariant($"{groupTimingPoint.Time},"));
writer.Write(FormattableString.Invariant($"{groupTimingPoint.BeatLength},")); writer.Write(FormattableString.Invariant($"{groupTimingPoint.BeatLength},"));
outputControlPointAt(groupTimingPoint.Time, true); outputControlPointAt(controlPointProperties, true);
lastControlPointProperties = controlPointProperties;
lastControlPointProperties.SliderVelocity = 1;
} }
if (controlPointProperties.IsRedundant(lastControlPointProperties))
continue;
// Output any remaining effects as secondary non-timing control point. // Output any remaining effects as secondary non-timing control point.
var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time);
writer.Write(FormattableString.Invariant($"{group.Time},")); writer.Write(FormattableString.Invariant($"{group.Time},"));
writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SliderVelocity},")); writer.Write(FormattableString.Invariant($"{-100 / controlPointProperties.SliderVelocity},"));
outputControlPointAt(group.Time, false); outputControlPointAt(controlPointProperties, false);
lastControlPointProperties = controlPointProperties;
} }
void outputControlPointAt(double time, bool isTimingPoint) LegacyControlPointProperties getLegacyControlPointProperties(ControlPointGroup group, bool updateSampleBank)
{ {
var samplePoint = legacyControlPoints.SamplePointAt(time); var timingPoint = legacyControlPoints.TimingPointAt(group.Time);
var effectPoint = legacyControlPoints.EffectPointAt(time); var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time);
var timingPoint = legacyControlPoints.TimingPointAt(time); var samplePoint = legacyControlPoints.SamplePointAt(group.Time);
var effectPoint = legacyControlPoints.EffectPointAt(group.Time);
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty)); HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty));
int customSampleBank = toLegacyCustomSampleBank(tempHitSample);
// Convert effect flags to the legacy format // Convert effect flags to the legacy format
LegacyEffectFlags effectFlags = LegacyEffectFlags.None; LegacyEffectFlags effectFlags = LegacyEffectFlags.None;
@ -234,12 +242,26 @@ namespace osu.Game.Beatmaps.Formats
if (timingPoint.OmitFirstBarLine) if (timingPoint.OmitFirstBarLine)
effectFlags |= LegacyEffectFlags.OmitFirstBarLine; effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
writer.Write(FormattableString.Invariant($"{timingPoint.TimeSignature.Numerator},")); return new LegacyControlPointProperties
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); {
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},")); SliderVelocity = difficultyPoint.SliderVelocity,
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); TimingSignature = timingPoint.TimeSignature.Numerator,
writer.Write(FormattableString.Invariant($"{(isTimingPoint ? '1' : '0')},")); SampleBank = updateSampleBank ? (int)toLegacySampleBank(tempHitSample.Bank) : lastControlPointProperties.SampleBank,
writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); // Inherit the previous custom sample bank if the current custom sample bank is not set
CustomSampleBank = customSampleBank >= 0 ? customSampleBank : lastControlPointProperties.CustomSampleBank,
SampleVolume = tempHitSample.Volume,
EffectFlags = effectFlags
};
}
void outputControlPointAt(LegacyControlPointProperties controlPoint, bool isTimingPoint)
{
writer.Write(FormattableString.Invariant($"{controlPoint.TimingSignature.ToString(CultureInfo.InvariantCulture)},"));
writer.Write(FormattableString.Invariant($"{controlPoint.SampleBank.ToString(CultureInfo.InvariantCulture)},"));
writer.Write(FormattableString.Invariant($"{controlPoint.CustomSampleBank.ToString(CultureInfo.InvariantCulture)},"));
writer.Write(FormattableString.Invariant($"{controlPoint.SampleVolume.ToString(CultureInfo.InvariantCulture)},"));
writer.Write(FormattableString.Invariant($"{(isTimingPoint ? "1" : "0")},"));
writer.Write(FormattableString.Invariant($"{((int)controlPoint.EffectFlags).ToString(CultureInfo.InvariantCulture)}"));
writer.WriteLine(); writer.WriteLine();
} }
@ -249,7 +271,10 @@ namespace osu.Game.Beatmaps.Formats
yield break; yield break;
foreach (var hitObject in hitObjects) foreach (var hitObject in hitObjects)
yield return hitObject.DifficultyControlPoint; {
if (hitObject is IHasSliderVelocity hasSliderVelocity)
yield return new DifficultyControlPoint { Time = hitObject.StartTime, SliderVelocity = hasSliderVelocity.SliderVelocity };
}
} }
void extractDifficultyControlPoints(IEnumerable<HitObject> hitObjects) void extractDifficultyControlPoints(IEnumerable<HitObject> hitObjects)
@ -268,7 +293,15 @@ namespace osu.Game.Beatmaps.Formats
{ {
foreach (var hitObject in hitObjects) foreach (var hitObject in hitObjects)
{ {
yield return hitObject.SampleControlPoint; if (hitObject.Samples.Count > 0)
{
int volume = hitObject.Samples.Max(o => o.Volume);
int customIndex = hitObject.Samples.Any(o => o is ConvertHitObjectParser.LegacyHitSampleInfo)
? hitObject.Samples.OfType<ConvertHitObjectParser.LegacyHitSampleInfo>().Max(o => o.CustomSampleBank)
: -1;
yield return new LegacyBeatmapDecoder.LegacySampleControlPoint { Time = hitObject.GetEndTime(), SampleVolume = volume, CustomSampleBank = customIndex };
}
foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects)) foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects))
yield return nested; yield return nested;
@ -466,16 +499,16 @@ namespace osu.Game.Beatmaps.Formats
if (curveData != null) if (curveData != null)
{ {
for (int i = 0; i < curveData.NodeSamples.Count; i++) for (int i = 0; i < curveData.SpanCount() + 1; i++)
{ {
writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); writer.Write(FormattableString.Invariant($"{(i < curveData.NodeSamples.Count ? (int)toLegacyHitSoundType(curveData.NodeSamples[i]) : 0)}"));
writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); writer.Write(i != curveData.SpanCount() ? "|" : ",");
} }
for (int i = 0; i < curveData.NodeSamples.Count; i++) for (int i = 0; i < curveData.SpanCount() + 1; i++)
{ {
writer.Write(getSampleBank(curveData.NodeSamples[i], true)); writer.Write(i < curveData.NodeSamples.Count ? getSampleBank(curveData.NodeSamples[i], true) : "0:0");
writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); writer.Write(i != curveData.SpanCount() ? "|" : ",");
} }
} }
} }
@ -506,10 +539,18 @@ namespace osu.Game.Beatmaps.Formats
if (!banksOnly) if (!banksOnly)
{ {
string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))); int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty;
int volume = samples.FirstOrDefault()?.Volume ?? 100; int volume = samples.FirstOrDefault()?.Volume ?? 100;
// We want to ignore custom sample banks and volume when not encoding to the mania game mode,
// because they cause unexpected results in the editor and are already satisfied by the control points.
if (onlineRulesetID != 3)
{
customSampleBank = 0;
volume = 0;
}
sb.Append(':'); sb.Append(':');
sb.Append(FormattableString.Invariant($"{customSampleBank}:")); sb.Append(FormattableString.Invariant($"{customSampleBank}:"));
sb.Append(FormattableString.Invariant($"{volume}:")); sb.Append(FormattableString.Invariant($"{volume}:"));
@ -562,12 +603,30 @@ namespace osu.Game.Beatmaps.Formats
} }
} }
private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) private int toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo)
{ {
if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy) if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture); return legacy.CustomSampleBank;
return "0"; return 0;
}
private struct LegacyControlPointProperties
{
internal double SliderVelocity { get; set; }
internal int TimingSignature { get; init; }
internal int SampleBank { get; init; }
internal int CustomSampleBank { get; init; }
internal int SampleVolume { get; init; }
internal LegacyEffectFlags EffectFlags { get; init; }
internal bool IsRedundant(LegacyControlPointProperties other) =>
SliderVelocity == other.SliderVelocity &&
TimingSignature == other.TimingSignature &&
SampleBank == other.SampleBank &&
CustomSampleBank == other.CustomSampleBank &&
SampleVolume == other.SampleVolume &&
EffectFlags == other.EffectFlags;
} }
} }
} }

View File

@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Edit.Checks
yield break; yield break;
// Samples that allow themselves to be overridden by control points have a volume of 0. // Samples that allow themselves to be overridden by control points have a volume of 0.
int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume); int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume);
double samplePlayTime = sampledHitObject.GetEndTime(); double samplePlayTime = sampledHitObject.GetEndTime();
EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime); EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime);

View File

@ -23,6 +23,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.OSD; using osu.Game.Overlays.OSD;
using osu.Game.Overlays.Settings.Sections; using osu.Game.Overlays.Settings.Sections;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Components.TernaryButtons;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
@ -239,7 +240,7 @@ namespace osu.Game.Rulesets.Edit
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
{ {
return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1
/ BeatSnapProvider.BeatDivisor); / BeatSnapProvider.BeatDivisor);
} }

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Edit
HitObject = hitObject; HitObject = hitObject;
// adding the default hit sample should be the case regardless of the ruleset. // adding the default hit sample should be the case regardless of the ruleset.
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK, volume: 100));
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -74,9 +74,10 @@ namespace osu.Game.Rulesets.Edit
/// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param> /// <param name="commitStart">Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.</param>
protected void BeginPlacement(bool commitStart = false) protected void BeginPlacement(bool commitStart = false)
{ {
var nearestSampleControlPoint = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.SampleControlPoint?.DeepClone() as SampleControlPoint; // Take the hitnormal sample of the last hit object
var lastHitNormal = beatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
HitObject.SampleControlPoint = nearestSampleControlPoint ?? new SampleControlPoint(); if (lastHitNormal != null)
HitObject.Samples[0] = lastHitNormal;
placementHandler.BeginPlacement(HitObject); placementHandler.BeginPlacement(HitObject);
if (commitStart) if (commitStart)

View File

@ -357,13 +357,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (samples.Length <= 0) if (samples.Length <= 0)
return; return;
if (HitObject.SampleControlPoint == null) Samples.Samples = samples.Cast<ISampleInfo>().ToArray();
{
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
Samples.Samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
} }
private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples();

View File

@ -16,7 +16,6 @@ using osu.Framework.Lists;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -77,8 +76,11 @@ namespace osu.Game.Rulesets.Objects
/// </summary> /// </summary>
public virtual IList<HitSampleInfo> AuxiliarySamples => ImmutableList<HitSampleInfo>.Empty; public virtual IList<HitSampleInfo> AuxiliarySamples => ImmutableList<HitSampleInfo>.Empty;
public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT; /// <summary>
public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT; /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
/// DO NOT USE THIS UNLESS 100% SURE.
/// </summary>
public double? LegacyBpmMultiplier { get; set; }
/// <summary> /// <summary>
/// Whether this <see cref="HitObject"/> is in Kiai time. /// Whether this <see cref="HitObject"/> is in Kiai time.
@ -105,25 +107,8 @@ namespace osu.Game.Rulesets.Objects
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default)
{ {
var legacyInfo = controlPointInfo as LegacyControlPointInfo;
if (legacyInfo != null)
DifficultyControlPoint = (DifficultyControlPoint)legacyInfo.DifficultyPointAt(StartTime).DeepClone();
else if (ReferenceEquals(DifficultyControlPoint, DifficultyControlPoint.DEFAULT))
DifficultyControlPoint = new DifficultyControlPoint();
DifficultyControlPoint.Time = StartTime;
ApplyDefaultsToSelf(controlPointInfo, difficulty); ApplyDefaultsToSelf(controlPointInfo, difficulty);
// This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time.
if (legacyInfo != null)
SampleControlPoint = (SampleControlPoint)legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency).DeepClone();
else if (ReferenceEquals(SampleControlPoint, SampleControlPoint.DEFAULT))
SampleControlPoint = new SampleControlPoint();
SampleControlPoint.Time = this.GetEndTime() + control_point_leniency;
nestedHitObjects.Clear(); nestedHitObjects.Clear();
CreateNestedHitObjects(cancellationToken); CreateNestedHitObjects(cancellationToken);
@ -164,9 +149,6 @@ namespace osu.Game.Rulesets.Objects
foreach (var nested in nestedHitObjects) foreach (var nested in nestedHitObjects)
nested.StartTime += offset; nested.StartTime += offset;
DifficultyControlPoint.Time = time.NewValue;
SampleControlPoint.Time = this.GetEndTime() + control_point_leniency;
} }
} }
@ -222,6 +204,17 @@ namespace osu.Game.Rulesets.Objects
return slidingSamples; return slidingSamples;
} }
/// <summary>
/// Create a SampleInfo based on the sample settings of the hit normal sample in <see cref="Samples"/>.
/// </summary>
/// <param name="sampleName">The name of the sample.</param>
/// <returns>A populated <see cref="HitSampleInfo"/>.</returns>
protected HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL)
{
var hitnormalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
return hitnormalSample == null ? new HitSampleInfo(sampleName) : hitnormalSample.With(newName: sampleName);
}
} }
public static class HitObjectExtensions public static class HitObjectExtensions

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
} }
if (split.Length > 10) if (split.Length > 10)
readCustomSampleBanks(split[10], bankInfo); readCustomSampleBanks(split[10], bankInfo, true);
// One node for each repeat + the start and end nodes // One node for each repeat + the start and end nodes
int nodes = repeatCount + 2; int nodes = repeatCount + 2;
@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
return result; return result;
} }
private void readCustomSampleBanks(string str, SampleBankInfo bankInfo) private void readCustomSampleBanks(string str, SampleBankInfo bankInfo, bool banksOnly = false)
{ {
if (string.IsNullOrEmpty(str)) if (string.IsNullOrEmpty(str))
return; return;
@ -202,6 +202,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
bankInfo.BankForNormal = stringBank; bankInfo.BankForNormal = stringBank;
bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank; bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
if (banksOnly) return;
if (split.Length > 2) if (split.Length > 2)
bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]); bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]);

View File

@ -6,13 +6,14 @@
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Objects.Legacy namespace osu.Game.Rulesets.Objects.Legacy
{ {
internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, IHasSliderVelocity
{ {
/// <summary> /// <summary>
/// Scoring distance with a speed-adjusted beat length of 1 second. /// Scoring distance with a speed-adjusted beat length of 1 second.
@ -40,13 +41,21 @@ namespace osu.Game.Rulesets.Objects.Legacy
public double Velocity = 1; public double Velocity = 1;
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1);
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
} }

View File

@ -0,0 +1,17 @@
// 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.
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
/// A type of <see cref="HitObject"/> which explicitly specifies whether it should generate ticks.
/// </summary>
public interface IHasGenerateTicks
{
/// <summary>
/// Whether or not slider ticks should be generated by this object.
/// This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991).
/// </summary>
public bool GenerateTicks { get; set; }
}
}

View File

@ -0,0 +1,19 @@
// 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 osu.Framework.Bindables;
namespace osu.Game.Rulesets.Objects.Types;
/// <summary>
/// A HitObject that has a slider velocity multiplier.
/// </summary>
public interface IHasSliderVelocity
{
/// <summary>
/// The slider velocity multiplier.
/// </summary>
double SliderVelocity { get; set; }
BindableNumber<double> SliderVelocityBindable { get; }
}

View File

@ -52,7 +52,6 @@ namespace osu.Game.Rulesets.UI
return; return;
var samples = nextObject.Samples var samples = nextObject.Samples
.Select(s => nextObject.SampleControlPoint.ApplyTo(s))
.Cast<ISampleInfo>() .Cast<ISampleInfo>()
.ToArray(); .ToArray();

View File

@ -293,10 +293,10 @@ namespace osu.Game.Rulesets.UI
{ {
// prepare sample pools ahead of time so we're not initialising at runtime. // prepare sample pools ahead of time so we're not initialising at runtime.
foreach (var sample in hitObject.Samples) foreach (var sample in hitObject.Samples)
prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample)); prepareSamplePool(sample);
foreach (var sample in hitObject.AuxiliarySamples) foreach (var sample in hitObject.AuxiliarySamples)
prepareSamplePool(hitObject.SampleControlPoint.ApplyTo(sample)); prepareSamplePool(sample);
foreach (var nestedObject in hitObject.NestedHitObjects) foreach (var nestedObject in hitObject.NestedHitObjects)
preloadSamples(nestedObject); preloadSamples(nestedObject);

View File

@ -13,12 +13,14 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
@ -29,13 +31,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly BindableNumber<double> speedMultiplier; private readonly BindableNumber<double> speedMultiplier;
public DifficultyPointPiece(HitObject hitObject) public DifficultyPointPiece(HitObject hitObject)
: base(hitObject.DifficultyControlPoint)
{ {
HitObject = hitObject; HitObject = hitObject;
speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy(); speedMultiplier = (hitObject as IHasSliderVelocity)?.SliderVelocityBindable.GetBoundCopy();
} }
protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -78,7 +81,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Spacing = new Vector2(0, 15), Spacing = new Vector2(0, 15),
Children = new Drawable[] Children = new Drawable[]
{ {
sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput<double>("Velocity", new DifficultyControlPoint().SliderVelocityBindable) sliderVelocitySlider = new IndeterminateSliderWithTextBoxInput<double>("Velocity", new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
})
{ {
KeyboardStep = 0.1f KeyboardStep = 0.1f
}, },
@ -94,11 +102,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).Where(o => o is IHasSliderVelocity).ToArray();
var relevantControlPoints = relevantObjects.Select(h => h.DifficultyControlPoint).ToArray();
// even if there are multiple objects selected, we can still display a value if they all have the same value. // even if there are multiple objects selected, we can still display a value if they all have the same value.
var selectedPointBindable = relevantControlPoints.Select(point => point.SliderVelocity).Distinct().Count() == 1 ? relevantControlPoints.First().SliderVelocityBindable : null; var selectedPointBindable = relevantObjects.Select(point => ((IHasSliderVelocity)point).SliderVelocity).Distinct().Count() == 1 ? ((IHasSliderVelocity)relevantObjects.First()).SliderVelocityBindable : null;
if (selectedPointBindable != null) if (selectedPointBindable != null)
{ {
@ -117,7 +124,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
foreach (var h in relevantObjects) foreach (var h in relevantObjects)
{ {
h.DifficultyControlPoint.SliderVelocity = val.NewValue.Value; ((IHasSliderVelocity)h).SliderVelocity = val.NewValue.Value;
beatmap.Update(h); beatmap.Update(h);
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
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.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK.Graphics; using osuTK.Graphics;
@ -16,21 +15,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public partial class HitObjectPointPiece : CircularContainer public partial class HitObjectPointPiece : CircularContainer
{ {
private readonly ControlPoint point;
protected OsuSpriteText Label { get; private set; } protected OsuSpriteText Label { get; private set; }
protected HitObjectPointPiece(ControlPoint point)
{
this.point = point;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Color4 colour = point.GetRepresentingColour(colours); Color4 colour = GetRepresentingColour(colours);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -61,5 +53,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}, },
}; };
} }
protected virtual Color4 GetRepresentingColour(OsuColour colours)
{
return colours.Yellow;
}
} }
} }

View File

@ -12,11 +12,13 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Timing;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
@ -24,22 +26,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public readonly HitObject HitObject; public readonly HitObject HitObject;
private readonly Bindable<string> bank; private readonly BindableList<HitSampleInfo> samplesBindable;
private readonly BindableNumber<int> volume;
public SamplePointPiece(HitObject hitObject) public SamplePointPiece(HitObject hitObject)
: base(hitObject.SampleControlPoint)
{ {
HitObject = hitObject; HitObject = hitObject;
volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy(); samplesBindable = hitObject.SamplesBindable.GetBoundCopy();
bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy();
} }
protected override Color4 GetRepresentingColour(OsuColour colours) => colours.Pink;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
volume.BindValueChanged(_ => updateText()); samplesBindable.BindCollectionChanged((_, _) => updateText(), true);
bank.BindValueChanged(_ => updateText(), true);
} }
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
@ -50,7 +50,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateText() private void updateText()
{ {
Label.Text = $"{bank.Value} {volume.Value}"; Label.Text = $"{GetBankValue(samplesBindable)} {GetVolumeValue(samplesBindable)}";
}
public static string? GetBankValue(IEnumerable<HitSampleInfo> samples)
{
return samples.FirstOrDefault()?.Bank;
}
public static int GetVolumeValue(ICollection<HitSampleInfo> samples)
{
return samples.Count == 0 ? 0 : samples.Max(o => o.Volume);
} }
public Popover GetPopover() => new SampleEditPopover(HitObject); public Popover GetPopover() => new SampleEditPopover(HitObject);
@ -89,7 +99,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
Label = "Bank Name", Label = "Bank Name",
}, },
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new SampleControlPoint().SampleVolumeBindable) volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new BindableInt(100)
{
MinValue = 0,
MaxValue = 100,
})
} }
} }
}; };
@ -100,14 +114,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();
var relevantControlPoints = relevantObjects.Select(h => h.SampleControlPoint).ToArray(); var relevantSamples = relevantObjects.Select(h => h.Samples).ToArray();
// even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value. // even if there are multiple objects selected, we can still display sample volume or bank if they all have the same value.
string? commonBank = getCommonBank(relevantControlPoints); string? commonBank = getCommonBank(relevantSamples);
if (!string.IsNullOrEmpty(commonBank)) if (!string.IsNullOrEmpty(commonBank))
bank.Current.Value = commonBank; bank.Current.Value = commonBank;
int? commonVolume = getCommonVolume(relevantControlPoints); int? commonVolume = getCommonVolume(relevantSamples);
if (commonVolume != null) if (commonVolume != null)
volume.Current.Value = commonVolume.Value; volume.Current.Value = commonVolume.Value;
@ -117,9 +131,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
updateBankFor(relevantObjects, val.NewValue); updateBankFor(relevantObjects, val.NewValue);
updateBankPlaceholderText(relevantObjects); updateBankPlaceholderText(relevantObjects);
}); });
// on commit, ensure that the value is correct by sourcing it from the objects' control points again. // on commit, ensure that the value is correct by sourcing it from the objects' samples again.
// this ensures that committing empty text causes a revert to the previous value. // this ensures that committing empty text causes a revert to the previous value.
bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantControlPoints); bank.OnCommit += (_, _) => bank.Current.Value = getCommonBank(relevantSamples);
volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue)); volume.Current.BindValueChanged(val => updateVolumeFor(relevantObjects, val.NewValue));
} }
@ -130,8 +144,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume)); ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume));
} }
private static string? getCommonBank(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleBank).Distinct().Count() == 1 ? relevantControlPoints.First().SampleBank : null; private static string? getCommonBank(IList<HitSampleInfo>[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null;
private static int? getCommonVolume(SampleControlPoint[] relevantControlPoints) => relevantControlPoints.Select(point => point.SampleVolume).Distinct().Count() == 1 ? relevantControlPoints.First().SampleVolume : null; private static int? getCommonVolume(IList<HitSampleInfo>[] relevantSamples) => relevantSamples.Select(GetVolumeValue).Distinct().Count() == 1 ? GetVolumeValue(relevantSamples.First()) : null;
private void updateBankFor(IEnumerable<HitObject> objects, string? newBank) private void updateBankFor(IEnumerable<HitObject> objects, string? newBank)
{ {
@ -142,7 +156,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
foreach (var h in objects) foreach (var h in objects)
{ {
h.SampleControlPoint.SampleBank = newBank; for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newBank: newBank);
}
beatmap.Update(h); beatmap.Update(h);
} }
@ -151,7 +169,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateBankPlaceholderText(IEnumerable<HitObject> objects) private void updateBankPlaceholderText(IEnumerable<HitObject> objects)
{ {
string? commonBank = getCommonBank(objects.Select(h => h.SampleControlPoint).ToArray()); string? commonBank = getCommonBank(objects.Select(h => h.Samples).ToArray());
bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty; bank.PlaceholderText = string.IsNullOrEmpty(commonBank) ? "(multiple)" : string.Empty;
} }
@ -164,7 +182,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
foreach (var h in objects) foreach (var h in objects)
{ {
h.SampleControlPoint.SampleVolume = newVolume.Value; for (int i = 0; i < h.Samples.Count; i++)
{
h.Samples[i] = h.Samples[i].With(newVolume: newVolume.Value);
}
beatmap.Update(h); beatmap.Update(h);
} }

View File

@ -15,7 +15,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -102,6 +101,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}, },
} }
}, },
new SamplePointPiece(Item)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopCentre
},
}); });
if (item is IHasDuration) if (item is IHasDuration)
@ -111,6 +115,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
OnDragHandled = e => OnDragHandled?.Invoke(e) OnDragHandled = e => OnDragHandled?.Invoke(e)
}); });
} }
if (item is IHasSliderVelocity)
{
AddInternal(new DifficultyPointPiece(Item)
{
Anchor = Anchor.TopLeft,
Origin = Anchor.BottomCentre
});
}
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -187,12 +200,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
colouredComponents.Colour = OsuColour.ForegroundTextColourFor(averageColour); colouredComponents.Colour = OsuColour.ForegroundTextColourFor(averageColour);
} }
private SamplePointPiece? sampleOverrideDisplay;
private DifficultyPointPiece? difficultyOverrideDisplay;
private DifficultyControlPoint difficultyControlPoint = null!;
private SampleControlPoint sampleControlPoint = null!;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -208,36 +215,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (Item is IHasRepeats repeats) if (Item is IHasRepeats repeats)
updateRepeats(repeats); updateRepeats(repeats);
} }
if (!ReferenceEquals(difficultyControlPoint, Item.DifficultyControlPoint))
{
difficultyControlPoint = Item.DifficultyControlPoint;
difficultyOverrideDisplay?.Expire();
if (Item.DifficultyControlPoint != null && Item is IHasDistance)
{
AddInternal(difficultyOverrideDisplay = new DifficultyPointPiece(Item)
{
Anchor = Anchor.TopLeft,
Origin = Anchor.BottomCentre
});
}
}
if (!ReferenceEquals(sampleControlPoint, Item.SampleControlPoint))
{
sampleControlPoint = Item.SampleControlPoint;
sampleOverrideDisplay?.Expire();
if (Item.SampleControlPoint != null)
{
AddInternal(sampleOverrideDisplay = new SamplePointPiece(Item)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopCentre
});
}
}
} }
private void updateRepeats(IHasRepeats repeats) private void updateRepeats(IHasRepeats repeats)
@ -395,17 +372,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
case IHasRepeats repeatHitObject: case IHasRepeats repeatHitObject:
double proposedDuration = time - hitObject.StartTime; double proposedDuration = time - hitObject.StartTime;
if (e.CurrentState.Keyboard.ShiftPressed) if (e.CurrentState.Keyboard.ShiftPressed && hitObject is IHasSliderVelocity hasSliderVelocity)
{ {
if (ReferenceEquals(hitObject.DifficultyControlPoint, DifficultyControlPoint.DEFAULT)) double newVelocity = hasSliderVelocity.SliderVelocity * (repeatHitObject.Duration / proposedDuration);
hitObject.DifficultyControlPoint = new DifficultyControlPoint();
double newVelocity = hitObject.DifficultyControlPoint.SliderVelocity * (repeatHitObject.Duration / proposedDuration); if (Precision.AlmostEquals(newVelocity, hasSliderVelocity.SliderVelocity))
if (Precision.AlmostEquals(newVelocity, hitObject.DifficultyControlPoint.SliderVelocity))
return; return;
hitObject.DifficultyControlPoint.SliderVelocity = newVelocity; hasSliderVelocity.SliderVelocity = newVelocity;
beatmap.Update(hitObject); beatmap.Update(hitObject);
} }
else else