// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable 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.Input.Events; using osu.Framework.Lists; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; 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 partial class DrawableScrollingRuleset : DrawableRuleset, IDrawableScrollingRuleset, 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 = 20000; /// /// 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) { MinValue = time_span_min, MaxValue = time_span_max }; /// /// 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); public 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() { updateScrollAlgorithm(); double lastObjectTime = Beatmap.HitObjects.Any() ? Beatmap.GetLastObjectTime() : double.MaxValue; double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH; if (RelativeScaleBeatLengths) { baseBeatLength = Beatmap.GetMostCommonBeatLength(); // 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 /= Beatmap.Difficulty.SliderMultiplier; } // Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point var lastTimingPoint = new TimingControlPoint(); var lastEffectPoint = new EffectControlPoint(); var allPoints = new SortedList(Comparer.Default); allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints); allPoints.AddRange(Beatmap.ControlPointInfo.EffectPoints); // Generate the timing points, making non-timing changes use the previous timing change and vice-versa var timingChanges = allPoints.Select(c => { switch (c) { case TimingControlPoint timingPoint: lastTimingPoint = timingPoint; break; case EffectControlPoint difficultyPoint: lastEffectPoint = difficultyPoint; break; } return new MultiplierControlPoint(c.Time) { Velocity = Beatmap.Difficulty.SliderMultiplier, BaseBeatLength = baseBeatLength, TimingPoint = lastTimingPoint, EffectPoint = lastEffectPoint }; }); // Trim unwanted sequences of timing changes timingChanges = timingChanges // Collapse sections after the last hit object .Where(s => s.Time <= lastObjectTime) // Collapse sections with the same start time .GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time); ControlPoints.AddRange(timingChanges); if (ControlPoints.Count == 0) ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.Difficulty.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)}."); } private ScrollVisualisationMethod visualisationMethod = ScrollVisualisationMethod.Sequential; public ScrollVisualisationMethod VisualisationMethod { get => visualisationMethod; set { visualisationMethod = value; updateScrollAlgorithm(); } } private void updateScrollAlgorithm() { switch (VisualisationMethod) { case ScrollVisualisationMethod.Sequential: scrollingInfo.Algorithm.Value = new SequentialScrollAlgorithm(ControlPoints); break; case ScrollVisualisationMethod.Overlapping: scrollingInfo.Algorithm.Value = new OverlappingScrollAlgorithm(ControlPoints); break; case ScrollVisualisationMethod.Constant: scrollingInfo.Algorithm.Value = new ConstantScrollAlgorithm(); break; } } /// /// 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(KeyBindingPressEvent e) { if (!UserScrollSpeedAdjustment) return false; switch (e.Action) { case GlobalAction.IncreaseScrollSpeed: AdjustScrollSpeed(1); return true; case GlobalAction.DecreaseScrollSpeed: AdjustScrollSpeed(-1); return true; } return false; } private ScheduledDelegate scheduledScrollSpeedAdjustment; public void OnReleased(KeyBindingReleaseEvent e) { scheduledScrollSpeedAdjustment?.Cancel(); scheduledScrollSpeedAdjustment = null; } private class LocalScrollingInfo : IScrollingInfo { public IBindable Direction { get; } = new Bindable(); public IBindable TimeRange { get; } = new BindableDouble(); public readonly Bindable Algorithm = new Bindable(new ConstantScrollAlgorithm()); IBindable IScrollingInfo.Algorithm => Algorithm; } } }