mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 07:06:35 +09:00
Merge branch 'master' into cinema-mod
This commit is contained in:
@ -1,12 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.Configuration
|
||||
{
|
||||
public abstract class RulesetConfigManager<T> : DatabasedConfigManager<T>, IRulesetConfigManager
|
||||
where T : struct
|
||||
public abstract class RulesetConfigManager<TLookup> : DatabasedConfigManager<TLookup>, IRulesetConfigManager
|
||||
where TLookup : struct, Enum
|
||||
{
|
||||
protected RulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
|
||||
: base(settings, ruleset, variant)
|
||||
|
@ -4,8 +4,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
@ -41,10 +41,10 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
|
||||
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
||||
|
||||
var clock = new StopwatchClock();
|
||||
mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock));
|
||||
var track = new TrackVirtual(10000);
|
||||
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
|
||||
return calculate(playableBeatmap, mods, clock.Rate);
|
||||
return calculate(playableBeatmap, mods, track.Rate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -3,8 +3,8 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
@ -35,9 +35,9 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
|
||||
protected virtual void ApplyMods(Mod[] mods)
|
||||
{
|
||||
var clock = new StopwatchClock();
|
||||
mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock));
|
||||
TimeRate = clock.Rate;
|
||||
var track = new TrackVirtual(10000);
|
||||
mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
TimeRate = track.Rate;
|
||||
}
|
||||
|
||||
public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);
|
||||
|
@ -34,7 +34,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
where TObject : HitObject
|
||||
{
|
||||
protected IRulesetConfigManager Config { get; private set; }
|
||||
protected EditorBeatmap<TObject> EditorBeatmap { get; private set; }
|
||||
|
||||
protected new EditorBeatmap<TObject> EditorBeatmap { get; private set; }
|
||||
|
||||
protected readonly Ruleset Ruleset;
|
||||
|
||||
[Resolved]
|
||||
@ -148,7 +150,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap);
|
||||
|
||||
EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
|
||||
base.EditorBeatmap = EditorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
|
||||
EditorBeatmap.HitObjectAdded += addHitObject;
|
||||
EditorBeatmap.HitObjectRemoved += removeHitObject;
|
||||
EditorBeatmap.StartTimeChanged += UpdateHitObject;
|
||||
@ -333,6 +335,11 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// </summary>
|
||||
public abstract IEnumerable<DrawableHitObject> HitObjects { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An editor-specific beatmap, exposing mutation events.
|
||||
/// </summary>
|
||||
public IEditorBeatmap EditorBeatmap { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user's cursor is currently in an area of the <see cref="HitObjectComposer"/> that is valid for placement.
|
||||
/// </summary>
|
||||
|
@ -1,15 +1,15 @@
|
||||
// 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.Timing;
|
||||
using osu.Framework.Audio.Track;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface for mods that make adjustments to the track.
|
||||
/// </summary>
|
||||
public interface IApplicableToClock : IApplicableMod
|
||||
public interface IApplicableToTrack : IApplicableMod
|
||||
{
|
||||
void ApplyToClock(IAdjustableClock clock);
|
||||
void ApplyToTrack(Track track);
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
// 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.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -14,12 +13,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage Icon => FontAwesome.Solid.Question;
|
||||
public override string Description => "Whoaaaaa...";
|
||||
|
||||
public override void ApplyToClock(IAdjustableClock clock)
|
||||
public override void ApplyToTrack(Track track)
|
||||
{
|
||||
if (clock is IHasPitchAdjust pitchAdjust)
|
||||
pitchAdjust.PitchAdjust *= RateAdjust;
|
||||
else
|
||||
base.ApplyToClock(clock);
|
||||
track.Frequency.Value *= RateAdjust;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModDoubleTime : ModTimeAdjust, IApplicableToClock
|
||||
public abstract class ModDoubleTime : ModTimeAdjust
|
||||
{
|
||||
public override string Name => "Double Time";
|
||||
public override string Acronym => "DT";
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModHalfTime : ModTimeAdjust, IApplicableToClock
|
||||
public abstract class ModHalfTime : ModTimeAdjust
|
||||
{
|
||||
public override string Name => "Half Time";
|
||||
public override string Acronym => "HT";
|
||||
|
@ -1,9 +1,8 @@
|
||||
// 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.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
@ -15,12 +14,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage Icon => OsuIcon.ModNightcore;
|
||||
public override string Description => "Uguuuuuuuu...";
|
||||
|
||||
public override void ApplyToClock(IAdjustableClock clock)
|
||||
public override void ApplyToTrack(Track track)
|
||||
{
|
||||
if (clock is IHasPitchAdjust pitchAdjust)
|
||||
pitchAdjust.PitchAdjust *= RateAdjust;
|
||||
else
|
||||
base.ApplyToClock(clock);
|
||||
track.Frequency.Value *= RateAdjust;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,23 +2,19 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Audio.Track;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModTimeAdjust : Mod
|
||||
public abstract class ModTimeAdjust : Mod, IApplicableToTrack
|
||||
{
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) };
|
||||
|
||||
protected abstract double RateAdjust { get; }
|
||||
|
||||
public virtual void ApplyToClock(IAdjustableClock clock)
|
||||
public virtual void ApplyToTrack(Track track)
|
||||
{
|
||||
if (clock is IHasTempoAdjust tempo)
|
||||
tempo.TempoAdjust *= RateAdjust;
|
||||
else
|
||||
clock.Rate *= RateAdjust;
|
||||
track.Tempo.Value *= RateAdjust;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,14 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToClock, IApplicableToBeatmap
|
||||
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToTrack, IApplicableToBeatmap
|
||||
{
|
||||
/// <summary>
|
||||
/// The point in the beatmap at which the final ramping rate should be reached.
|
||||
@ -24,11 +23,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
private double finalRateTime;
|
||||
private double beginRampTime;
|
||||
private IAdjustableClock clock;
|
||||
private Track track;
|
||||
|
||||
public virtual void ApplyToClock(IAdjustableClock clock)
|
||||
public virtual void ApplyToTrack(Track track)
|
||||
{
|
||||
this.clock = clock;
|
||||
this.track = track;
|
||||
|
||||
lastAdjust = 1;
|
||||
|
||||
@ -46,7 +45,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public virtual void Update(Playfield playfield)
|
||||
{
|
||||
applyAdjustment((clock.CurrentTime - beginRampTime) / finalRateTime);
|
||||
applyAdjustment((track.CurrentTime - beginRampTime) / finalRateTime);
|
||||
}
|
||||
|
||||
private double lastAdjust = 1;
|
||||
@ -59,23 +58,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
double adjust = 1 + (Math.Sign(FinalRateAdjustment) * Math.Clamp(amount, 0, 1) * Math.Abs(FinalRateAdjustment));
|
||||
|
||||
switch (clock)
|
||||
{
|
||||
case IHasPitchAdjust pitch:
|
||||
pitch.PitchAdjust /= lastAdjust;
|
||||
pitch.PitchAdjust *= adjust;
|
||||
break;
|
||||
|
||||
case IHasTempoAdjust tempo:
|
||||
tempo.TempoAdjust /= lastAdjust;
|
||||
tempo.TempoAdjust *= adjust;
|
||||
break;
|
||||
|
||||
default:
|
||||
clock.Rate /= lastAdjust;
|
||||
clock.Rate *= adjust;
|
||||
break;
|
||||
}
|
||||
track.Tempo.Value /= lastAdjust;
|
||||
track.Tempo.Value *= adjust;
|
||||
|
||||
lastAdjust = adjust;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using osuTK;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
};
|
||||
}
|
||||
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
||||
List<IList<HitSampleInfo>> nodeSamples)
|
||||
{
|
||||
newCombo |= forceNewCombo;
|
||||
@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
X = position.X,
|
||||
NewCombo = FirstObject || newCombo,
|
||||
ComboOffset = comboOffset,
|
||||
Path = new SliderPath(pathType, controlPoints, length),
|
||||
Path = new SliderPath(controlPoints, length),
|
||||
NodeSamples = nodeSamples,
|
||||
RepeatCount = repeatCount
|
||||
};
|
||||
|
@ -115,12 +115,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos;
|
||||
}
|
||||
|
||||
// osu-stable special-cased colinear perfect curves to a CurveType.Linear
|
||||
static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
|
||||
|
||||
if (points.Length == 3 && pathType == PathType.PerfectCurve && isLinear(points))
|
||||
pathType = PathType.Linear;
|
||||
|
||||
int repeatCount = Parsing.ParseInt(split[6]);
|
||||
|
||||
if (repeatCount > 9000)
|
||||
@ -187,7 +181,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
for (int i = 0; i < nodes; i++)
|
||||
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
|
||||
|
||||
result = CreateSlider(pos, combo, comboOffset, points, length, pathType, repeatCount, nodeSamples);
|
||||
result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples);
|
||||
|
||||
// The samples are played when the slider ends, which is the last node
|
||||
result.Samples = nodeSamples[nodeSamples.Count - 1];
|
||||
@ -259,6 +253,44 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
bankInfo.Filename = split.Length > 4 ? split[4] : null;
|
||||
}
|
||||
|
||||
private PathControlPoint[] convertControlPoints(Vector2[] vertices, PathType type)
|
||||
{
|
||||
if (type == PathType.PerfectCurve)
|
||||
{
|
||||
if (vertices.Length != 3)
|
||||
type = PathType.Bezier;
|
||||
else if (isLinear(vertices))
|
||||
{
|
||||
// osu-stable special-cased colinear perfect curves to a linear path
|
||||
type = PathType.Linear;
|
||||
}
|
||||
}
|
||||
|
||||
var points = new List<PathControlPoint>(vertices.Length)
|
||||
{
|
||||
new PathControlPoint
|
||||
{
|
||||
Position = { Value = vertices[0] },
|
||||
Type = { Value = type }
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 1; i < vertices.Length; i++)
|
||||
{
|
||||
if (vertices[i] == vertices[i - 1])
|
||||
{
|
||||
points[points.Count - 1].Type.Value = type;
|
||||
continue;
|
||||
}
|
||||
|
||||
points.Add(new PathControlPoint { Position = { Value = vertices[i] } });
|
||||
}
|
||||
|
||||
return points.ToArray();
|
||||
|
||||
static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a legacy Hit-type hit object.
|
||||
/// </summary>
|
||||
@ -276,11 +308,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
|
||||
/// <param name="controlPoints">The slider control points.</param>
|
||||
/// <param name="length">The slider length.</param>
|
||||
/// <param name="pathType">The slider curve type.</param>
|
||||
/// <param name="repeatCount">The slider repeat count.</param>
|
||||
/// <param name="nodeSamples">The samples to be played when the slider nodes are hit. This includes the head and tail of the slider.</param>
|
||||
/// <returns>The hit object.</returns>
|
||||
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
|
||||
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
||||
List<IList<HitSampleInfo>> nodeSamples);
|
||||
|
||||
/// <summary>
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using osuTK;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
||||
@ -26,13 +25,13 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
||||
};
|
||||
}
|
||||
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
||||
List<IList<HitSampleInfo>> nodeSamples)
|
||||
{
|
||||
return new ConvertSlider
|
||||
{
|
||||
X = position.X,
|
||||
Path = new SliderPath(pathType, controlPoints, length),
|
||||
Path = new SliderPath(controlPoints, length),
|
||||
NodeSamples = nodeSamples,
|
||||
RepeatCount = repeatCount
|
||||
};
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
|
||||
@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||
};
|
||||
}
|
||||
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
||||
List<IList<HitSampleInfo>> nodeSamples)
|
||||
{
|
||||
newCombo |= forceNewCombo;
|
||||
@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||
Position = position,
|
||||
NewCombo = FirstObject || newCombo,
|
||||
ComboOffset = comboOffset,
|
||||
Path = new SliderPath(pathType, controlPoints, length),
|
||||
Path = new SliderPath(controlPoints, length),
|
||||
NodeSamples = nodeSamples,
|
||||
RepeatCount = repeatCount
|
||||
};
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Audio;
|
||||
|
||||
@ -23,12 +22,12 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
||||
return new ConvertHit();
|
||||
}
|
||||
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double? length, PathType pathType, int repeatCount,
|
||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
|
||||
List<IList<HitSampleInfo>> nodeSamples)
|
||||
{
|
||||
return new ConvertSlider
|
||||
{
|
||||
Path = new SliderPath(pathType, controlPoints, length),
|
||||
Path = new SliderPath(controlPoints, length),
|
||||
NodeSamples = nodeSamples,
|
||||
RepeatCount = repeatCount
|
||||
};
|
||||
|
52
osu.Game/Rulesets/Objects/PathControlPoint.cs
Normal file
52
osu.Game/Rulesets/Objects/PathControlPoint.cs
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
public class PathControlPoint : IEquatable<PathControlPoint>
|
||||
{
|
||||
/// <summary>
|
||||
/// The position of this <see cref="PathControlPoint"/>.
|
||||
/// </summary>
|
||||
public readonly Bindable<Vector2> Position = new Bindable<Vector2>();
|
||||
|
||||
/// <summary>
|
||||
/// The type of path segment starting at this <see cref="PathControlPoint"/>.
|
||||
/// If null, this <see cref="PathControlPoint"/> will be a part of the previous path segment.
|
||||
/// </summary>
|
||||
public readonly Bindable<PathType?> Type = new Bindable<PathType?>();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when any property of this <see cref="PathControlPoint"/> is changed.
|
||||
/// </summary>
|
||||
internal event Action Changed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PathControlPoint"/>.
|
||||
/// </summary>
|
||||
public PathControlPoint()
|
||||
{
|
||||
Position.ValueChanged += _ => Changed?.Invoke();
|
||||
Type.ValueChanged += _ => Changed?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PathControlPoint"/> with a provided position and type.
|
||||
/// </summary>
|
||||
/// <param name="position">The initial position.</param>
|
||||
/// <param name="type">The initial type.</param>
|
||||
public PathControlPoint(Vector2 position, PathType? type = null)
|
||||
: this()
|
||||
{
|
||||
Position.Value = position;
|
||||
Type.Value = type;
|
||||
}
|
||||
|
||||
public bool Equals(PathControlPoint other) => Position.Value == other?.Position.Value && Type.Value == other.Type.Value;
|
||||
}
|
||||
}
|
@ -1,68 +1,86 @@
|
||||
// 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.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
public struct SliderPath : IEquatable<SliderPath>
|
||||
public class SliderPath
|
||||
{
|
||||
/// <summary>
|
||||
/// The current version of this <see cref="SliderPath"/>. Updated when any change to the path occurs.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IBindable<int> Version => version;
|
||||
|
||||
private readonly Bindable<int> version = new Bindable<int>();
|
||||
|
||||
/// <summary>
|
||||
/// The user-set distance of the path. If non-null, <see cref="Distance"/> will match this value,
|
||||
/// and the path will be shortened/lengthened to match this length.
|
||||
/// </summary>
|
||||
public readonly double? ExpectedDistance;
|
||||
|
||||
/// <summary>
|
||||
/// The type of path.
|
||||
/// </summary>
|
||||
public readonly PathType Type;
|
||||
|
||||
[JsonProperty]
|
||||
private Vector2[] controlPoints;
|
||||
|
||||
private List<Vector2> calculatedPath;
|
||||
private List<double> cumulativeLength;
|
||||
|
||||
private bool isInitialised;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SliderPath"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of path.</param>
|
||||
/// <param name="controlPoints">The control points of the path.</param>
|
||||
/// <param name="expectedDistance">A user-set distance of the path that may be shorter or longer than the true distance between all
|
||||
/// <paramref name="controlPoints"/>. The path will be shortened/lengthened to match this length.
|
||||
/// If null, the path will use the true distance between all <paramref name="controlPoints"/>.</param>
|
||||
[JsonConstructor]
|
||||
public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null)
|
||||
{
|
||||
this = default;
|
||||
this.controlPoints = controlPoints;
|
||||
|
||||
Type = type;
|
||||
ExpectedDistance = expectedDistance;
|
||||
|
||||
ensureInitialised();
|
||||
}
|
||||
public readonly Bindable<double?> ExpectedDistance = new Bindable<double?>();
|
||||
|
||||
/// <summary>
|
||||
/// The control points of the path.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ReadOnlySpan<Vector2> ControlPoints
|
||||
public readonly BindableList<PathControlPoint> ControlPoints = new BindableList<PathControlPoint>();
|
||||
|
||||
private readonly List<Vector2> calculatedPath = new List<Vector2>();
|
||||
private readonly List<double> cumulativeLength = new List<double>();
|
||||
private readonly Cached pathCache = new Cached();
|
||||
|
||||
private double calculatedLength;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SliderPath"/>.
|
||||
/// </summary>
|
||||
public SliderPath()
|
||||
{
|
||||
get
|
||||
ExpectedDistance.ValueChanged += _ => invalidate();
|
||||
|
||||
ControlPoints.ItemsAdded += items =>
|
||||
{
|
||||
ensureInitialised();
|
||||
return controlPoints.AsSpan();
|
||||
}
|
||||
foreach (var c in items)
|
||||
c.Changed += invalidate;
|
||||
|
||||
invalidate();
|
||||
};
|
||||
|
||||
ControlPoints.ItemsRemoved += items =>
|
||||
{
|
||||
foreach (var c in items)
|
||||
c.Changed -= invalidate;
|
||||
|
||||
invalidate();
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SliderPath"/> initialised with a list of control points.
|
||||
/// </summary>
|
||||
/// <param name="controlPoints">An optional set of <see cref="PathControlPoint"/>s to initialise the path with.</param>
|
||||
/// <param name="expectedDistance">A user-set distance of the path that may be shorter or longer than the true distance between all control points.
|
||||
/// The path will be shortened/lengthened to match this length. If null, the path will use the true distance between all control points.</param>
|
||||
[JsonConstructor]
|
||||
public SliderPath(PathControlPoint[] controlPoints, double? expectedDistance = null)
|
||||
: this()
|
||||
{
|
||||
ControlPoints.AddRange(controlPoints);
|
||||
ExpectedDistance.Value = expectedDistance;
|
||||
}
|
||||
|
||||
public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null)
|
||||
: this(controlPoints.Select((c, i) => new PathControlPoint(c, i == 0 ? (PathType?)type : null)).ToArray(), expectedDistance)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -73,11 +91,23 @@ namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
get
|
||||
{
|
||||
ensureInitialised();
|
||||
ensureValid();
|
||||
return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The distance of the path prior to lengthening/shortening to account for <see cref="ExpectedDistance"/>.
|
||||
/// </summary>
|
||||
public double CalculatedDistance
|
||||
{
|
||||
get
|
||||
{
|
||||
ensureValid();
|
||||
return calculatedLength;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
|
||||
/// to 1 (end of the slider) and stores the generated path in the given list.
|
||||
@ -87,7 +117,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
|
||||
public void GetPathToProgress(List<Vector2> path, double p0, double p1)
|
||||
{
|
||||
ensureInitialised();
|
||||
ensureValid();
|
||||
|
||||
double d0 = progressToDistance(p0);
|
||||
double d1 = progressToDistance(p1);
|
||||
@ -116,40 +146,73 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// <returns></returns>
|
||||
public Vector2 PositionAt(double progress)
|
||||
{
|
||||
ensureInitialised();
|
||||
ensureValid();
|
||||
|
||||
double d = progressToDistance(progress);
|
||||
return interpolateVertices(indexOfDistance(d), d);
|
||||
}
|
||||
|
||||
private void ensureInitialised()
|
||||
private void invalidate()
|
||||
{
|
||||
if (isInitialised)
|
||||
return;
|
||||
|
||||
isInitialised = true;
|
||||
|
||||
controlPoints ??= Array.Empty<Vector2>();
|
||||
calculatedPath = new List<Vector2>();
|
||||
cumulativeLength = new List<double>();
|
||||
|
||||
calculatePath();
|
||||
calculateCumulativeLength();
|
||||
pathCache.Invalidate();
|
||||
version.Value++;
|
||||
}
|
||||
|
||||
private List<Vector2> calculateSubpath(ReadOnlySpan<Vector2> subControlPoints)
|
||||
private void ensureValid()
|
||||
{
|
||||
switch (Type)
|
||||
if (pathCache.IsValid)
|
||||
return;
|
||||
|
||||
calculatePath();
|
||||
calculateLength();
|
||||
|
||||
pathCache.Validate();
|
||||
}
|
||||
|
||||
private void calculatePath()
|
||||
{
|
||||
calculatedPath.Clear();
|
||||
|
||||
if (ControlPoints.Count == 0)
|
||||
return;
|
||||
|
||||
Vector2[] vertices = new Vector2[ControlPoints.Count];
|
||||
for (int i = 0; i < ControlPoints.Count; i++)
|
||||
vertices[i] = ControlPoints[i].Position.Value;
|
||||
|
||||
int start = 0;
|
||||
|
||||
for (int i = 0; i < ControlPoints.Count; i++)
|
||||
{
|
||||
if (ControlPoints[i].Type.Value == null && i < ControlPoints.Count - 1)
|
||||
continue;
|
||||
|
||||
// The current vertex ends the segment
|
||||
var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
|
||||
var segmentType = ControlPoints[start].Type.Value ?? PathType.Linear;
|
||||
|
||||
foreach (Vector2 t in calculateSubPath(segmentVertices, segmentType))
|
||||
{
|
||||
if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
|
||||
calculatedPath.Add(t);
|
||||
}
|
||||
|
||||
// Start the new segment at the current vertex
|
||||
start = i;
|
||||
}
|
||||
}
|
||||
|
||||
private List<Vector2> calculateSubPath(ReadOnlySpan<Vector2> subControlPoints, PathType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case PathType.Linear:
|
||||
return PathApproximator.ApproximateLinear(subControlPoints);
|
||||
|
||||
case PathType.PerfectCurve:
|
||||
//we can only use CircularArc iff we have exactly three control points and no dissection.
|
||||
if (ControlPoints.Length != 3 || subControlPoints.Length != 3)
|
||||
if (subControlPoints.Length != 3)
|
||||
break;
|
||||
|
||||
// Here we have exactly 3 control points. Attempt to fit a circular arc.
|
||||
List<Vector2> subpath = PathApproximator.ApproximateCircularArc(subControlPoints);
|
||||
|
||||
// If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.
|
||||
@ -165,74 +228,49 @@ namespace osu.Game.Rulesets.Objects
|
||||
return PathApproximator.ApproximateBezier(subControlPoints);
|
||||
}
|
||||
|
||||
private void calculatePath()
|
||||
private void calculateLength()
|
||||
{
|
||||
calculatedPath.Clear();
|
||||
|
||||
// Sliders may consist of various subpaths separated by two consecutive vertices
|
||||
// with the same position. The following loop parses these subpaths and computes
|
||||
// their shape independently, consecutively appending them to calculatedPath.
|
||||
|
||||
int start = 0;
|
||||
int end = 0;
|
||||
|
||||
for (int i = 0; i < ControlPoints.Length; ++i)
|
||||
{
|
||||
end++;
|
||||
|
||||
if (i == ControlPoints.Length - 1 || ControlPoints[i] == ControlPoints[i + 1])
|
||||
{
|
||||
ReadOnlySpan<Vector2> cpSpan = ControlPoints.Slice(start, end - start);
|
||||
|
||||
foreach (Vector2 t in calculateSubpath(cpSpan))
|
||||
{
|
||||
if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
|
||||
calculatedPath.Add(t);
|
||||
}
|
||||
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateCumulativeLength()
|
||||
{
|
||||
double l = 0;
|
||||
|
||||
calculatedLength = 0;
|
||||
cumulativeLength.Clear();
|
||||
cumulativeLength.Add(l);
|
||||
cumulativeLength.Add(0);
|
||||
|
||||
for (int i = 0; i < calculatedPath.Count - 1; ++i)
|
||||
for (int i = 0; i < calculatedPath.Count - 1; i++)
|
||||
{
|
||||
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
|
||||
double d = diff.Length;
|
||||
|
||||
// Shorted slider paths that are too long compared to the expected distance
|
||||
if (ExpectedDistance.HasValue && ExpectedDistance - l < d)
|
||||
{
|
||||
calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((ExpectedDistance - l) / d);
|
||||
calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i);
|
||||
|
||||
l = ExpectedDistance.Value;
|
||||
cumulativeLength.Add(l);
|
||||
break;
|
||||
}
|
||||
|
||||
l += d;
|
||||
cumulativeLength.Add(l);
|
||||
calculatedLength += diff.Length;
|
||||
cumulativeLength.Add(calculatedLength);
|
||||
}
|
||||
|
||||
// Lengthen slider paths that are too short compared to the expected distance
|
||||
if (ExpectedDistance.HasValue && l < ExpectedDistance && calculatedPath.Count > 1)
|
||||
if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
|
||||
{
|
||||
Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2];
|
||||
double d = diff.Length;
|
||||
// The last length is always incorrect
|
||||
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
|
||||
|
||||
if (d <= 0)
|
||||
int pathEndIndex = calculatedPath.Count - 1;
|
||||
|
||||
if (calculatedLength > expectedDistance)
|
||||
{
|
||||
// The path will be shortened further, in which case we should trim any more unnecessary lengths and their associated path segments
|
||||
while (cumulativeLength.Count > 0 && cumulativeLength[cumulativeLength.Count - 1] >= expectedDistance)
|
||||
{
|
||||
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
|
||||
calculatedPath.RemoveAt(pathEndIndex--);
|
||||
}
|
||||
}
|
||||
|
||||
if (pathEndIndex <= 0)
|
||||
{
|
||||
// The expected distance is negative or zero
|
||||
// TODO: Perhaps negative path lengths should be disallowed altogether
|
||||
cumulativeLength.Add(0);
|
||||
return;
|
||||
}
|
||||
|
||||
calculatedPath[calculatedPath.Count - 1] += diff * (float)((ExpectedDistance - l) / d);
|
||||
cumulativeLength[calculatedPath.Count - 1] = ExpectedDistance.Value;
|
||||
// The direction of the segment to shorten or lengthen
|
||||
Vector2 dir = (calculatedPath[pathEndIndex] - calculatedPath[pathEndIndex - 1]).Normalized();
|
||||
|
||||
calculatedPath[pathEndIndex] = calculatedPath[pathEndIndex - 1] + dir * (float)(expectedDistance - cumulativeLength[cumulativeLength.Count - 1]);
|
||||
cumulativeLength.Add(expectedDistance);
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,15 +310,5 @@ namespace osu.Game.Rulesets.Objects
|
||||
double w = (d - d0) / (d1 - d0);
|
||||
return p0 + (p1 - p0) * (float)w;
|
||||
}
|
||||
|
||||
public bool Equals(SliderPath other)
|
||||
{
|
||||
if (ControlPoints == null && other.ControlPoints != null)
|
||||
return false;
|
||||
if (other.ControlPoints == null && ControlPoints != null)
|
||||
return false;
|
||||
|
||||
return ControlPoints.SequenceEqual(other.ControlPoints) && ExpectedDistance.Equals(other.ExpectedDistance) && Type == other.Type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -331,6 +332,9 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
protected override bool OnHover(HoverEvent e) => true; // required for IProvideCursor
|
||||
|
||||
// only show the cursor when within the playfield, by default.
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Playfield.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
CursorContainer IProvideCursor.Cursor => Playfield.Cursor;
|
||||
|
||||
public override GameplayCursorContainer Cursor => Playfield.Cursor;
|
||||
@ -507,15 +511,19 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
|
||||
|
||||
public void AddAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => throw new NotImplementedException();
|
||||
public void AddAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
|
||||
|
||||
public void RemoveAdjustment(AdjustableProperty type, BindableDouble adjustBindable) => throw new NotImplementedException();
|
||||
public void RemoveAdjustment(AdjustableProperty type, BindableNumber<double> adjustBindable) => throw new NotImplementedException();
|
||||
|
||||
public BindableDouble Volume => throw new NotImplementedException();
|
||||
public BindableNumber<double> Volume => throw new NotImplementedException();
|
||||
|
||||
public BindableDouble Balance => throw new NotImplementedException();
|
||||
public BindableNumber<double> Balance => throw new NotImplementedException();
|
||||
|
||||
public BindableDouble Frequency => throw new NotImplementedException();
|
||||
public BindableNumber<double> Frequency => throw new NotImplementedException();
|
||||
|
||||
public BindableNumber<double> Tempo => throw new NotImplementedException();
|
||||
|
||||
public IBindable<double> GetAggregate(AdjustableProperty type) => throw new NotImplementedException();
|
||||
|
||||
public IBindable<double> AggregateVolume => throw new NotImplementedException();
|
||||
|
||||
@ -523,6 +531,8 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
public IBindable<double> AggregateFrequency => throw new NotImplementedException();
|
||||
|
||||
public IBindable<double> AggregateTempo => throw new NotImplementedException();
|
||||
|
||||
public int PlaybackConcurrency
|
||||
{
|
||||
get => throw new NotImplementedException();
|
||||
|
@ -100,10 +100,13 @@ namespace osu.Game.Rulesets.UI
|
||||
public GameplayCursorContainer Cursor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Provide an optional cursor which is to be used for gameplay.
|
||||
/// Provide a cursor which is to be used for gameplay.
|
||||
/// </summary>
|
||||
/// <returns>The cursor, or null if a cursor is not rqeuired.</returns>
|
||||
protected virtual GameplayCursorContainer CreateCursor() => null;
|
||||
/// <remarks>
|
||||
/// The default provided cursor is invisible when inside the bounds of the <see cref="Playfield"/>.
|
||||
/// </remarks>
|
||||
/// <returns>The cursor, or null to show the menu cursor.</returns>
|
||||
protected virtual GameplayCursorContainer CreateCursor() => new InvisibleCursorContainer();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>.
|
||||
@ -143,5 +146,14 @@ namespace osu.Game.Rulesets.UI
|
||||
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
|
||||
|
||||
public class InvisibleCursorContainer : GameplayCursorContainer
|
||||
{
|
||||
protected override Drawable CreateCursor() => new InvisibleCursor();
|
||||
|
||||
private class InvisibleCursor : Drawable
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user