// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. 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.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; 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 ScrollingRulesetContainer : RulesetContainer, IKeyBindingHandler where TObject : HitObject where TPlayfield : ScrollingPlayfield { /// /// 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; /// /// Provides the default s that adjust the scrolling rate of s /// inside this . /// /// private readonly SortedList controlPoints = new SortedList(Comparer.Default); protected IScrollingInfo ScrollingInfo => scrollingInfo; [Cached(Type = typeof(IScrollingInfo))] private readonly LocalScrollingInfo scrollingInfo; protected ScrollingRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { scrollingInfo = new LocalScrollingInfo(); scrollingInfo.Direction.BindTo(Direction); scrollingInfo.TimeRange.BindTo(TimeRange); 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; } } [BackgroundDependencyLoader] private void load() { // Calculate default multiplier control points var lastTimingPoint = new TimingControlPoint(); var lastDifficultyPoint = new DifficultyControlPoint(); // Merge timing + difficulty points 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 var timingChanges = allPoints.Select(c => { var timingPoint = c as TimingControlPoint; var difficultyPoint = c as DifficultyControlPoint; if (timingPoint != null) lastTimingPoint = timingPoint; if (difficultyPoint != null) lastDifficultyPoint = difficultyPoint; return new MultiplierControlPoint(c.Time) { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier, TimingPoint = lastTimingPoint, DifficultyPoint = lastDifficultyPoint }; }); double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue; // Perform some post processing of the 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 we have no control points, add a default one if (controlPoints.Count == 0) controlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); } public bool OnPressed(GlobalAction action) { if (!UserScrollSpeedAdjustment) return false; switch (action) { case GlobalAction.IncreaseScrollSpeed: this.TransformBindableTo(TimeRange, TimeRange.Value - time_span_step, 200, Easing.OutQuint); return true; case GlobalAction.DecreaseScrollSpeed: this.TransformBindableTo(TimeRange, TimeRange.Value + time_span_step, 200, Easing.OutQuint); return true; } return false; } public bool OnReleased(GlobalAction action) => false; private class LocalScrollingInfo : IScrollingInfo { public IBindable Direction { get; } = new Bindable(); public IBindable TimeRange { get; } = new BindableDouble(); public IScrollAlgorithm Algorithm { get; set; } } } }