// Copyright (c) ppy Pty Ltd . 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Lists; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI.Scrolling.Algorithms; namespace osu.Game.Rulesets.UI.Scrolling { /// /// A type of that supports a . /// s inside this will scroll within the playfield. /// public abstract class DrawableScrollingRuleset : DrawableRuleset, IKeyBindingHandler where TObject : HitObject { /// /// The default span of time visible by the length of the scrolling axes. /// This is clamped between and . /// private const double time_span_default = 1500; /// /// The minimum span of time that may be visible by the length of the scrolling axes. /// private const double time_span_min = 50; /// /// The maximum span of time that may be visible by the length of the scrolling axes. /// private const double time_span_max = 10000; /// /// The step increase/decrease of the span of time visible by the length of the scrolling axes. /// private const double time_span_step = 200; protected readonly Bindable Direction = new Bindable(); /// /// The span of time that is visible by the length of the scrolling axes. /// For example, only hit objects with start time less than or equal to 1000 will be visible with = 1000. /// protected readonly BindableDouble TimeRange = new BindableDouble(time_span_default) { Default = time_span_default, MinValue = time_span_min, MaxValue = time_span_max }; protected virtual ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Sequential; /// /// Whether the player can change . /// protected virtual bool UserScrollSpeedAdjustment => true; /// /// Whether beat lengths should scale relative to the most common beat length in the . /// protected virtual bool RelativeScaleBeatLengths => false; /// /// The s that adjust the scrolling rate of s inside this . /// protected readonly SortedList ControlPoints = new SortedList(Comparer.Default); protected IScrollingInfo ScrollingInfo => scrollingInfo; [Cached(Type = typeof(IScrollingInfo))] private readonly LocalScrollingInfo scrollingInfo; protected DrawableScrollingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) : base(ruleset, beatmap, mods) { scrollingInfo = new LocalScrollingInfo(); scrollingInfo.Direction.BindTo(Direction); scrollingInfo.TimeRange.BindTo(TimeRange); } [BackgroundDependencyLoader] private void load() { switch (VisualisationMethod) { case ScrollVisualisationMethod.Sequential: scrollingInfo.Algorithm = new SequentialScrollAlgorithm(ControlPoints); break; case ScrollVisualisationMethod.Overlapping: scrollingInfo.Algorithm = new OverlappingScrollAlgorithm(ControlPoints); break; case ScrollVisualisationMethod.Constant: scrollingInfo.Algorithm = new ConstantScrollAlgorithm(); break; } double lastObjectTime = Objects.LastOrDefault()?.GetEndTime() ?? double.MaxValue; double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH; if (RelativeScaleBeatLengths) { IReadOnlyList timingPoints = Beatmap.ControlPointInfo.TimingPoints; double maxDuration = 0; for (int i = 0; i < timingPoints.Count; i++) { if (timingPoints[i].Time > lastObjectTime) break; double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time : lastObjectTime; double duration = endTime - timingPoints[i].Time; if (duration > maxDuration) { maxDuration = duration; // The slider multiplier is post-multiplied to determine the final velocity, but for relative scale beat lengths // the multiplier should not affect the effective timing point (the longest in the beatmap), so it is factored out here baseBeatLength = timingPoints[i].BeatLength / Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier; } } } // Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point var lastTimingPoint = new TimingControlPoint(); var lastDifficultyPoint = new DifficultyControlPoint(); var allPoints = new SortedList(Comparer.Default); allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints); allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints); // Generate the timing points, making non-timing changes use the previous timing change and vice-versa var timingChanges = allPoints.Select(c => { if (c is TimingControlPoint timingPoint) lastTimingPoint = timingPoint; else if (c is DifficultyControlPoint difficultyPoint) lastDifficultyPoint = difficultyPoint; return new MultiplierControlPoint(c.Time) { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier, BaseBeatLength = baseBeatLength, TimingPoint = lastTimingPoint, DifficultyPoint = lastDifficultyPoint }; }); // Trim unwanted sequences of timing changes timingChanges = timingChanges // Collapse sections after the last hit object .Where(s => s.StartTime <= lastObjectTime) // Collapse sections with the same start time .GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime); ControlPoints.AddRange(timingChanges); if (ControlPoints.Count == 0) ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); } protected override void LoadComplete() { base.LoadComplete(); if (!(Playfield is ScrollingPlayfield)) throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset)}."); } /// /// Adjusts the scroll speed of s. /// /// The amount to adjust by. Greater than 0 if the scroll speed should be increased, less than 0 if it should be decreased. protected virtual void AdjustScrollSpeed(int amount) => this.TransformBindableTo(TimeRange, TimeRange.Value - amount * time_span_step, 200, Easing.OutQuint); public bool OnPressed(GlobalAction action) { if (!UserScrollSpeedAdjustment) return false; switch (action) { case GlobalAction.IncreaseScrollSpeed: scheduleScrollSpeedAdjustment(1); return true; case GlobalAction.DecreaseScrollSpeed: scheduleScrollSpeedAdjustment(-1); return true; } return false; } private ScheduledDelegate scheduledScrollSpeedAdjustment; public void OnReleased(GlobalAction action) { scheduledScrollSpeedAdjustment?.Cancel(); scheduledScrollSpeedAdjustment = null; } private void scheduleScrollSpeedAdjustment(int amount) { scheduledScrollSpeedAdjustment?.Cancel(); scheduledScrollSpeedAdjustment = this.BeginKeyRepeat(Scheduler, () => AdjustScrollSpeed(amount)); } private class LocalScrollingInfo : IScrollingInfo { public IBindable Direction { get; } = new Bindable(); public IBindable TimeRange { get; } = new BindableDouble(); public IScrollAlgorithm Algorithm { get; set; } } } }